diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 95012ebc..b48f35ee 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://...) + } + } + } +} 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..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.domain.model.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 695d959c..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.domain.model.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/model/CalendarMonthlyInfo.kt b/app/src/main/java/com/sopt/clody/domain/model/CalendarMonthlyInfo.kt new file mode 100644 index 00000000..28d3b0df --- /dev/null +++ b/app/src/main/java/com/sopt/clody/domain/model/CalendarMonthlyInfo.kt @@ -0,0 +1,47 @@ +package com.sopt.clody.domain.model + +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() + 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..e7860c9c --- /dev/null +++ b/app/src/main/java/com/sopt/clody/domain/model/DailyDiaryInfo.kt @@ -0,0 +1,13 @@ +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/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/domain/model/ReplyStatus.kt b/app/src/main/java/com/sopt/clody/domain/type/ReplyStatus.kt similarity index 87% rename from app/src/main/java/com/sopt/clody/domain/model/ReplyStatus.kt rename to app/src/main/java/com/sopt/clody/domain/type/ReplyStatus.kt index f1c5885f..ced774b3 100644 --- a/app/src/main/java/com/sopt/clody/domain/model/ReplyStatus.kt +++ b/app/src/main/java/com/sopt/clody/domain/type/ReplyStatus.kt @@ -1,4 +1,4 @@ -package com.sopt.clody.domain.model +package com.sopt.clody.domain.type import kotlinx.serialization.Serializable 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/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/diarylist/component/DailyDiaryCard.kt b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/DailyDiaryCard.kt index ed6029f9..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 @@ -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.domain.type.ReplyStatus import com.sopt.clody.presentation.ui.diarylist.screen.DiaryListViewModel import com.sopt.clody.ui.theme.ClodyTheme @@ -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/diarylist/component/MonthlyDiaryList.kt b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/MonthlyDiaryList.kt index 401257a1..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.domain.model.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 fa10a207..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.domain.model.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 ae5e732a..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.domain.model.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/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/calendar/model/DiaryDateData.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/model/DiaryDateData.kt deleted file mode 100644 index b8d5b3aa..00000000 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/model/DiaryDateData.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.sopt.clody.presentation.ui.home.calendar.model - -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/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 69% 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..06ac528a 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 @@ -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 @@ -23,23 +22,20 @@ 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.DailyDiaryInfo import com.sopt.clody.ui.theme.ClodyTheme -import kotlinx.datetime.DayOfWeek import java.time.LocalDate import java.time.format.TextStyle +import java.util.Locale @Composable -fun DailyDiaryListItem( - date: LocalDate, - dayOfWeek: DayOfWeek, - dailyDiary: DailyDiariesResponseDto, - onShowDiaryDeleteStateChange: (Boolean) -> Unit, +fun DailyDiary( + selectedDate: LocalDate, + selectedDailyInfo: DailyDiaryInfo, + onClickDiaryDelete: () -> Unit, ) { Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), + modifier = Modifier.padding(horizontal = 16.dp), verticalArrangement = Arrangement.spacedBy(10.dp), ) { Row( @@ -49,38 +45,55 @@ fun DailyDiaryListItem( .padding(8.dp), ) { Text( - text = "${date.month.value}.${date.dayOfMonth}", + text = "${selectedDate.monthValue}.${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] }, + LocalConfiguration.current.locales.let { if (it.isEmpty) Locale.getDefault() else it[0] }, ), style = ClodyTheme.typography.body2SemiBold, color = ClodyTheme.colors.gray02, modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp), ) Spacer(modifier = Modifier.weight(1f)) - if (dailyDiary.diaries.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.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( @@ -92,11 +105,11 @@ fun DailyDiaryListItem( } } - dailyDiary.diaries.isEmpty() -> { + else -> { Box( contentAlignment = Alignment.Center, modifier = Modifier - .fillMaxSize() + .fillMaxWidth() .padding(vertical = 44.dp), ) { Text( @@ -107,31 +120,6 @@ fun DailyDiaryListItem( ) } } - - else -> { - dailyDiary.diaries.forEachIndexed { index, diary -> - DiaryItem(index = index + 1, text = diary.content) - } - } } } } - -@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/DiaryStateButton.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DailyStateButton.kt similarity index 66% 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..c1f3e049 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,43 +1,38 @@ 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.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 DiaryStateButton( - hasDraft: Boolean, - canWrite: Boolean, - canReply: Boolean, - isInvalidDraft: Boolean, - year: Int, - month: Int, - day: Int, - onClickWriteDiary: (Int, Int, Int) -> Unit, +fun DailyStateButton( + calendarDailyInfo: CalendarMonthlyInfo.CalendarDailyInfo, + selectedDailyInfo: DailyDiaryInfo, + onClickWriteDiary: () -> Unit, + onClickContinueDraft: () -> Unit, onClickReplyDiary: () -> Unit, + modifier: Modifier = Modifier, ) { - val modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) + val type = DailyStateButtonType.getType(calendarDailyInfo, selectedDailyInfo) - when { - hasDraft -> { + when (type) { + DailyStateButtonType.DRAFT_ENABLED -> { ClodyButton( - onClick = { onClickWriteDiary(year, month, day) }, + onClick = onClickContinueDraft, 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), @@ -48,7 +43,7 @@ fun DiaryStateButton( ) } - canReply -> { + DailyStateButtonType.REPLY_ENABLED -> { ClodyReplyButton( onClick = onClickReplyDiary, text = stringResource(R.string.home_btn_check_reply), @@ -57,9 +52,9 @@ fun DiaryStateButton( ) } - canWrite -> { + DailyStateButtonType.DIARY_ENABLED -> { ClodyButton( - onClick = { onClickWriteDiary(year, month, day) }, + onClick = onClickWriteDiary, text = stringResource(R.string.home_btn_write_diary), enabled = true, modifier = modifier, @@ -68,7 +63,7 @@ fun DiaryStateButton( 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/HomeTopAppBar.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/HomeTopAppBar.kt index 6bac1a49..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 @@ -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() }, ), @@ -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 new file mode 100644 index 00000000..8dea7ce4 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/MonthlyCalendar.kt @@ -0,0 +1,198 @@ +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.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 +import java.time.DayOfWeek +import java.time.LocalDate +import java.time.format.TextStyle + +@Composable +fun MonthlyCalendar( + year: Int, + month: Int, + selectedDate: LocalDate, + calendarDailyInfoList: List, + onClickDay: (Int) -> Unit, +) { + val screenWidth = LocalConfiguration.current.screenWidthDp.dp + val cellWidth = remember(screenWidth) { (screenWidth - 40.dp) / 7 } + 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) } } + 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, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + ) { + // 요일 헤더 + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp), + ) { + dayOfWeeks.forEach { dayOfWeek -> + Box( + modifier = Modifier.width(cellWidth), + contentAlignment = Alignment.Center, + ) { + Text( + text = dayOfWeek.getDisplayName(TextStyle.NARROW, LocalConfiguration.current.locales[0]), + 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(), + ) { + paddedDates.chunked(7).forEach { weekDates -> + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + weekDates.forEach { dateOrNull -> + Box( + modifier = Modifier + .weight(1f) + .padding(vertical = 2.dp), + contentAlignment = Alignment.Center, + ) { + if (dateOrNull != null) { + val calendarDailyInfo = infoByDate[dateOrNull.toString()] + if (calendarDailyInfo != null) { + DailyClover( + localDate = dateOrNull, + calendarDailyInfo = calendarDailyInfo, + onClickDay = { + AmplitudeUtils.trackEvent(AmplitudeConstraints.HOME_CALENDAR_CLOVER) + onClickDay(dateOrNull.dayOfMonth) + }, + isSelected = dateOrNull == selectedDate, + modifier = Modifier.fillMaxWidth(), + ) + } + } + } + } + if (weekDates.size < 7) { + repeat(7 - weekDates.size) { + Box( + modifier = Modifier + .width(cellWidth) + .padding(vertical = 2.dp), + ) + } + } + } + } + } + } + } +} + +@Composable +private fun DailyClover( + localDate: LocalDate, + calendarDailyInfo: CalendarMonthlyInfo.CalendarDailyInfo, + onClickDay: () -> Unit, + isSelected: Boolean, + modifier: Modifier = Modifier, +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = modifier + .padding(8.dp) + .clip(RoundedCornerShape(12.dp)) + .clickable { onClickDay() }, + ) { + Box( + modifier = Modifier + .size(48.dp), + contentAlignment = Alignment.Center, + ) { + Image( + painter = painterResource(id = DailyCloverType.getType(calendarDailyInfo).iconRes), + contentDescription = "Diary clover icon", + ) + if (calendarDailyInfo.replyStatus == ReplyStatus.READY_NOT_READ && calendarDailyInfo.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 = localDate.dayOfMonth.toString(), + style = ClodyTheme.typography.detail1SemiBold.copy( + color = when { + isSelected -> ClodyTheme.colors.white + localDate == LocalDate.now() -> 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..02d79bd2 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/MonthlyCalendarAndDailyDiary.kt @@ -0,0 +1,86 @@ +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.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.CalendarMonthlyInfo +import com.sopt.clody.domain.model.DailyDiaryInfo +import com.sopt.clody.ui.theme.ClodyTheme +import java.time.LocalDate + +@Composable +fun MonthlyCalendarAndDailyDiary( + year: Int, + month: Int, + selectedDate: LocalDate, + calendarMonthlyInfo: CalendarMonthlyInfo, + onClickDay: (Int) -> Unit, + selectedDailyInfo: DailyDiaryInfo, + onClickDiaryDelete: () -> Unit, + modifier: Modifier = Modifier, +) { + val scrollState = rememberScrollState() + + Column( + modifier = modifier + .fillMaxSize() + .verticalScroll(scrollState) + .background(ClodyTheme.colors.white), + ) { + // 클로버 총 갯수 + Text( + text = stringResource(R.string.home_total_clover, calendarMonthlyInfo.totalCloverCount), + 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( + year = year, + month = month, + selectedDate = selectedDate, + calendarDailyInfoList = calendarMonthlyInfo.calendarDailyInfoList, + onClickDay = onClickDay, + ) + + HorizontalDivider( + color = ClodyTheme.colors.gray08, + thickness = 6.dp, + modifier = Modifier + .fillMaxWidth() + .padding(top = 30.dp, bottom = 20.dp), + ) + + DailyDiary( + selectedDate = selectedDate, + selectedDailyInfo = selectedDailyInfo, + onClickDiaryDelete = onClickDiaryDelete, + ) + } + } +} 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..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.domain.model.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/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/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..905d21f9 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeContract.kt @@ -0,0 +1,78 @@ +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 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 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 showDraftExpiredDialog: Boolean = false, + val showInAppReviewPopup: Boolean = false, + val showDraftNotificationPopup: Boolean = false, + val showDraftNotificationToast: Boolean = false, + val errorDialogMessage: String? = null, + val errorScreenMessage: String? = null, + ) : MavericksState { + val selectedDate: LocalDate = LocalDate.of(year, month, dayOfMonth) + + fun getCalendarDailyInfo(): 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 InitializeInfo(val year: Int, val month: Int, val dayOfMonth: Int) : HomeIntent() + data object OnClickDiaryList : HomeIntent() + data object OnClickYearMonth : 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 class OnClickWriteDiary(val year: Int, val month: Int, val dayOfMonth: Int) : HomeIntent() + data class OnClickReplyDiary(val replyStatus: ReplyStatus) : HomeIntent() + data object ShowDraftExpiredDialog : 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 object EnableDraftAlarm : HomeIntent() + 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 { + data object NavigateToDiaryList : HomeSideEffect + data object NavigateToSetting : 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 83772e74..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 @@ -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 @@ -32,420 +32,319 @@ 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.MonthlyCalendarResponseDto -import com.sopt.clody.domain.model.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 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 -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.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 calendarState by homeViewModel.calendarState.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() + 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(effect.year, effect.month, effect.dayOfMonth) + is HomeContract.HomeSideEffect.NavigateToReplyLoading -> navigateToReplyLoading( + state.year, + state.month, + state.dayOfMonth, + Route.ReplyLoading.ReplyLoadingFrom.HOME, + effect.replyStatus, + ) + } + } + } } LaunchedEffect(Unit) { AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.HOME) + viewModel.postIntent(HomeContract.HomeIntent.InitializeInfo(state.year, state.month, state.dayOfMonth)) + } - if (showInAppReviewPopup && isFromReplyDiary) { - InAppReviewManager.showPopup(context as Activity) - homeViewModel.updateShowInAppReviewPopup(false) + var backPressedTime by remember { mutableLongStateOf(0L) } + val backPressThreshold = 2000 + BackHandler { + val currentTime = System.currentTimeMillis() + if (currentTime - backPressedTime <= backPressThreshold) { + (context as? Activity)?.finish() + } else { + backPressedTime = currentTime } } - // 알림 권한 요청 + val requestPermissionLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission(), + ) { isGranted: Boolean -> + viewModel.postIntent(HomeContract.HomeIntent.RequestNotificationPermission(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.RequestNotificationPermission(true)) } } else { - homeViewModel.sendNotification(true) + viewModel.postIntent(HomeContract.HomeIntent.RequestNotificationPermission(true)) } } - LaunchedEffect(Unit) { - homeViewModel.updateYearMonthAndLoadData( - selectedDiaryDate.year, - selectedDiaryDate.month, - selectedDate.dayOfMonth, - ) + LaunchedEffect(state.showInAppReviewPopup, isFromReplyDiary) { + if (state.showInAppReviewPopup && isFromReplyDiary) { + InAppReviewManager.showPopup(context as Activity) + viewModel.postIntent(HomeContract.HomeIntent.UpdateInAppReviewFlag(false)) + } } - if (isError) { + if (state.errorScreenMessage != null) { FailureScreen( - message = errorMessage, - confirmAction = { - homeViewModel.updateYearMonthAndLoadData( - selectedDiaryDate.year, - selectedDiaryDate.month, - selectedDate.dayOfMonth, - ) - }, + message = state.errorScreenMessage!!, + confirmAction = { viewModel.postIntent(HomeContract.HomeIntent.InitializeInfo(state.year, state.month, state.dayOfMonth)) }, ) } else { HomeScreen( - homeViewModel = homeViewModel, - calendarState = calendarState, - 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, - ) - }, - isError = isError, - errorMessage = errorMessage, - selectedYear = selectedDiaryDate.year, - selectedMonth = selectedDiaryDate.month, - selectedDate = selectedDate, - hasDraft = hasDraft, - canWrite = homeViewModel.canWriteDiary(), - canReply = homeViewModel.canReplyDiary(), - isInvalidDraft = replyStatus == ReplyStatus.INVALID_DRAFT, + state = state, + onIntent = { viewModel.postIntent(it) }, ) + } +} - 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)) - } +@Composable +fun HomeScreen( + 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) + onIntent(HomeContract.HomeIntent.OnClickDiaryList) }, - ) - } - - 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), - ) + onClickYearMonth = { + onIntent(HomeContract.HomeIntent.OnClickYearMonth) }, - ) - } - - 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) + onClickSetting = { + onIntent(HomeContract.HomeIntent.OnClickSetting) }, - confirmButtonColor = ClodyTheme.colors.mainYellow, - confirmButtonTextColor = ClodyTheme.colors.gray01, ) - } - - if (showDiaryDeleteState) { - DiaryDeleteSheet( - onDismiss = { homeViewModel.setShowDiaryDeleteState(false) }, - showDiaryDeleteDialog = { - AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.HOME_DELETE_DIARY) - homeViewModel.setShowDiaryDeleteDialog(true) - }, + }, + containerColor = ClodyTheme.colors.white, + content = { innerPadding -> + MonthlyCalendarAndDailyDiary( + year = state.year, + month = state.month, + selectedDate = state.selectedDate, + calendarMonthlyInfo = state.calendarMonthlyInfo, + onClickDay = { dayOfMonth -> onIntent(HomeContract.HomeIntent.OnClickDay(state.year, state.month, dayOfMonth)) }, + selectedDailyInfo = state.dailyDiaryInfo, + onClickDiaryDelete = { onIntent(HomeContract.HomeIntent.OnClickDiaryDelete) }, + modifier = Modifier.padding(innerPadding), ) - } + }, + bottomBar = { + state.getCalendarDailyInfo()?.let { calendarDailyInfo -> + DailyStateButton( + calendarDailyInfo = calendarDailyInfo, + selectedDailyInfo = state.dailyDiaryInfo, + onClickWriteDiary = { + AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.HOME_WRITING_DIARY) + 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(state.year, state.month, state.dayOfMonth)) + } else { + onIntent(HomeContract.HomeIntent.ShowDraftExpiredDialog) + } + }, + onClickReplyDiary = { + AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.HOME_REPLY) + onIntent(HomeContract.HomeIntent.OnClickReplyDiary(calendarDailyInfo.replyStatus)) + }, + modifier = Modifier + .fillMaxWidth() + .navigationBarsPadding() + .background(ClodyTheme.colors.white) + .padding(horizontal = 16.dp, vertical = 14.dp), + ) + } + }, + ) - 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) + if (state.showYearMonthPicker) { + ClodyPopupBottomSheet(onDismissRequest = { onIntent(HomeContract.HomeIntent.DismissYearMonthPicker) }) { + YearMonthPicker( + onDismissRequest = { onIntent(HomeContract.HomeIntent.DismissYearMonthPicker) }, + selectedYear = state.year, + selectedMonth = state.month, + onYearMonthSelected = { newYear, newMonth -> + onIntent(HomeContract.HomeIntent.ConfirmYearMonthPicker(newYear, newMonth)) }, - onDismiss = { - AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.HOME_NO_DELETE_DIARY) - homeViewModel.setShowDiaryDeleteDialog(false) - }, - confirmButtonColor = ClodyTheme.colors.red, - confirmButtonTextColor = ClodyTheme.colors.white, ) } } -} -@Composable -fun HomeScreen( - homeViewModel: HomeViewModel, - calendarState: CalendarState, - 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, - isError: Boolean, - errorMessage: String, - selectedYear: Int, - selectedMonth: Int, - selectedDate: LocalDate, - hasDraft: Boolean, - canWrite: Boolean, - canReply: Boolean, - isInvalidDraft: Boolean, -) { - if (isError) { - FailureScreen( - message = errorMessage, - confirmAction = { - homeViewModel.updateYearMonthAndLoadData(selectedYear, selectedMonth, selectedDate.dayOfMonth) + if (state.showDiaryDeleteBottomSheet) { + DiaryDeleteSheet( + onDismiss = { + onIntent(HomeContract.HomeIntent.DismissDiaryDelete) + }, + showDiaryDeleteDialog = { + AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.HOME_DELETE_DIARY) + onIntent(HomeContract.HomeIntent.ShowDiaryDeleteDialog) }, ) - } 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(), - ) + 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)) }, - containerColor = ClodyTheme.colors.white, - content = { innerPadding -> - when (calendarState) { - is CalendarState.Idle -> {} - - 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.Error -> { - homeViewModel.setErrorState(true, calendarState.message) - } - } - - when (deleteDiaryState) { - is DeleteDiaryState.Idle -> {} - - is DeleteDiaryState.Loading -> { - LoadingScreen() - } + onDismiss = { + AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.HOME_NO_DELETE_DIARY) + onIntent(HomeContract.HomeIntent.DismissDiaryDelete) + }, + confirmButtonColor = ClodyTheme.colors.red, + confirmButtonTextColor = ClodyTheme.colors.white, + ) + } - is DeleteDiaryState.Success -> {} + 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(state.year, state.month, state.dayOfMonth)) }, + onDismiss = { onIntent(HomeContract.HomeIntent.DismissDraftExpiredDialog) }, + confirmButtonColor = ClodyTheme.colors.mainYellow, + confirmButtonTextColor = ClodyTheme.colors.gray01, + ) + } - is DeleteDiaryState.Failure -> { - homeViewModel.setErrorState(true, stringResource(R.string.home_error_delete_diary)) - } - } - }, - bottomBar = { + if (state.showDraftNotificationPopup) { + ClodyPopupBottomSheet( + onDismissRequest = { onIntent(HomeContract.HomeIntent.UpdateDraftPopupFlag(false)) }, + content = { Column( + horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier - .navigationBarsPadding() - .background(ClodyTheme.colors.white), + .fillMaxWidth() + .padding(top = 20.dp) + .padding(horizontal = 16.dp), ) { - 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, - ) + 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.UpdateDraftPopupFlag(false)) }, + enabled = true, + modifier = Modifier.fillMaxWidth(), + ) + Text( + text = stringResource(R.string.bottom_sheet_home_initial_draft_skip), + modifier = Modifier + .clickable(onClick = { onIntent(HomeContract.HomeIntent.UpdateDraftPopupFlag(false)) }) + .padding(12.dp), + color = ClodyTheme.colors.gray05, + style = ClodyTheme.typography.body4Medium, ) - Spacer(modifier = Modifier.height(14.dp)) + Spacer(modifier = Modifier.height(4.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 (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 = { onIntent(HomeContract.HomeIntent.DismissDraftNotificationToast) }, + modifier = Modifier + .navigationBarsPadding() + .padding(40.dp), ) - } - } + }, + ) + } + + 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 b37f7330..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 @@ -1,40 +1,38 @@ 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.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.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.ui.home.calendar.model.DiaryDateData -import com.sopt.clody.presentation.ui.setting.notificationsetting.screen.NotificationChangeState +import com.sopt.clody.presentation.utils.base.UiLoadState 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.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope -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.sync.withLock 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, @@ -42,338 +40,228 @@ class HomeViewModel @Inject constructor( private val reviewRepository: ReviewRepository, private val errorMessageProvider: ErrorMessageProvider, private val networkConnectivityObserver: NetworkConnectivityObserver, -) : ViewModel() { +) : MavericksViewModel(initialState) { - private val _calendarState = MutableStateFlow>(CalendarState.Idle) - val calendarState: StateFlow> get() = _calendarState - - private val _dailyDiariesState = - MutableStateFlow>(DailyDiariesState.Idle) - val dailyDiariesState: StateFlow> get() = _dailyDiariesState - - 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 { + _intents + .receiveAsFlow() + .onEach(::handleIntent) + .launchIn(viewModelScope) initialize() } - 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.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) + 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(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.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(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.EnableDraftAlarm -> enableDraftAlarm() + 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) } + } } - 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) + private fun initialize() { + setState { + copy( + showInAppReviewPopup = reviewRepository.getShouldShowPopup(), + showDraftNotificationPopup = draftRepository.getIsFirstUse(), + ) } - _calendarState.value = result.fold( - onSuccess = { - setErrorState(false) - CalendarState.Success(it) - }, - onFailure = { - setErrorState(true, errorMessageProvider.getTemporaryError()) - CalendarState.Error(errorMessageProvider.getTemporaryError()) - }, - ) } - private suspend fun loadDailyDiariesData(year: Int, month: Int, date: Int) { - if (!isNetworkAvailable()) { - setErrorState(true, errorMessageProvider.getNetworkError()) + private suspend fun loadCalendarMonthlyInfo(year: Int, month: Int, dayOfMonth: Int = 1) { + setState { copy(calendarLoadState = UiLoadState.Loading, year = year, month = month) } + + if (networkConnectivityObserver.networkStatus.first() != NetworkStatus.Available) { + setState { copy(errorScreenMessage = errorMessageProvider.getNetworkError()) } return } - _dailyDiariesState.value = DailyDiariesState.Loading - val result = withContext(Dispatchers.IO) { - diaryRepository.getDailyDiariesData(year, month, date) - } - _dailyDiariesState.value = result.fold( - onSuccess = { dailyResponse -> - _hasDraft.value = dailyResponse.isDraft - _diaryCount.value = dailyResponse.diaries.size - _isDeleted.value = dailyResponse.isDeleted - setErrorState(false) - DailyDiariesState.Success(dailyResponse) + + withContext(Dispatchers.IO) { diaryRepository.getMonthlyCalendarData(year, month) }.fold( + onSuccess = { data -> + setState { + copy( + calendarLoadState = UiLoadState.Success, + calendarMonthlyInfo = CalendarMonthlyInfo( + totalCloverCount = data.totalCloverCount, + calendarDailyInfoList = data.diaries.map { + CalendarMonthlyInfo.CalendarDailyInfo( + diaryCount = it.diaryCount, + replyStatus = it.replyStatus, + date = it.date, + isDeleted = it.isDeleted, + ) + }, + ), + errorScreenMessage = null, + ) + } + loadDailyDiaryInfo(year, month, dayOfMonth) }, onFailure = { - setErrorState(true, errorMessageProvider.getTemporaryError()) - DailyDiariesState.Error(errorMessageProvider.getTemporaryError()) + setState { + copy( + calendarLoadState = UiLoadState.Error, + errorScreenMessage = errorMessageProvider.getServerError(), + ) + } }, ) } - 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 = { - loadCalendarData(year, month) - loadDailyDiariesData(year, month, day) - _diaryCount.value = 0 - _isDeleted.value = false - _replyStatus.value = ReplyStatus.UNREADY - DeleteDiaryState.Success - }, - onFailure = { - DeleteDiaryState.Failure(it.message ?: errorMessageProvider.getTemporaryError()) - }, - ) - } - } - - private val loadDataMutex = Mutex() - fun updateYearMonthAndLoadData(year: Int, month: Int) { - viewModelScope.launch { - loadDataMutex.withLock { - val sameYm = _selectedDiaryDate.value.year == year && - _selectedDiaryDate.value.month == month + private suspend fun loadDailyDiaryInfo(year: Int, month: Int, dayOfMonth: Int) { + setState { copy(dailyDiaryLoadState = UiLoadState.Loading, dayOfMonth = dayOfMonth) } - 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) + if (networkConnectivityObserver.networkStatus.first() != NetworkStatus.Available) { + setState { copy(errorScreenMessage = errorMessageProvider.getNetworkError()) } + return + } - coroutineScope { - awaitAll( - async { loadCalendarData(year, month) }, - async { loadDailyDiariesData(year, month, 1) }, + withContext(Dispatchers.IO) { diaryRepository.getDailyDiariesData(year, month, dayOfMonth) }.fold( + onSuccess = { data -> + setState { + copy( + dailyDiaryLoadState = UiLoadState.Success, + dailyDiaryInfo = DailyDiaryInfo( + diaryList = data.diaries.map { it.content }, + isDraft = data.isDraft, + ), + errorScreenMessage = null, ) } - } - } - } - - 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) }, + }, + onFailure = { + setState { + copy( + dailyDiaryLoadState = UiLoadState.Error, + errorScreenMessage = errorMessageProvider.getServerError(), ) } - } - } - } - - fun updateSelectedDate(date: LocalDate) { - _selectedDate.value = date - viewModelScope.launch { - loadDailyDiariesData(date.year, date.monthValue, date.dayOfMonth) - } - } - - 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 - } - - fun setShowYearMonthPickerState(state: Boolean) { - _showYearMonthPickerState.value = state - } - - fun setShowDiaryDeleteState(state: Boolean) { - _showDiaryDeleteState.value = state - } - - fun setShowDiaryDeleteDialog(state: Boolean) { - _showDiaryDeleteDialog.value = state - } - - fun setShowContinueDraftDialog(state: Boolean) { - _showContinueDraftDialog.value = state + }, + ) } - fun updateFirstDraftUse(newState: Boolean) { - draftRepository.setIsFirstUse(false) - _showFirstDraftPopup.value = newState - } + private suspend fun deleteDiary(year: Int, month: Int, dayOfMonth: Int) { + setState { copy(diaryDeleteState = UiLoadState.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 + if (networkConnectivityObserver.networkStatus.first() != NetworkStatus.Available) { + setState { copy(errorDialogMessage = errorMessageProvider.getNetworkError()) } + return } - return _diaryCount.value == 0 && isAvailableDay - } - fun canReplyDiary(): Boolean { - return _diaryCount.value > 0 && !_isDeleted.value - } - - fun isValidDraftDate(): Boolean { - val today = LocalDate.now() - val selected = _selectedDate.value - return selected == today || selected == today.minusDays(1) + withContext(Dispatchers.IO) { diaryRepository.deleteDailyDiary(year, month, dayOfMonth) }.fold( + onSuccess = { + loadCalendarMonthlyInfo(year, month, dayOfMonth) + setState { copy(showDiaryDeleteDialog = false, diaryDeleteState = UiLoadState.Success) } + }, + onFailure = { + setState { copy(diaryDeleteState = UiLoadState.Error, errorDialogMessage = errorMessageProvider.getServerError()) } + }, + ) } - fun enableDraftAlarm() { - viewModelScope.launch { - if (!isNetworkAvailable()) { - setErrorState(true, errorMessageProvider.getNetworkError()) - return@launch + private suspend fun getNotificationInfoOrNull(): NotificationInfoResponseDto? = + withContext(Dispatchers.IO) { notificationRepository.getNotificationInfo() } + .getOrElse { + setState { copy(errorDialogMessage = errorMessageProvider.getTemporaryError()) } + null } - val fcmToken = fcmTokenProvider.getToken().orEmpty() - val notificationInfo = getNotificationInfo() ?: return@launch - val request = buildDraftAlarmRequest(notificationInfo, fcmToken) - sendDraftAlarmRequest(request) - } + 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 isNetworkAvailable(): Boolean { - return networkConnectivityObserver.networkStatus.first() == NetworkStatus.Available - } + private suspend fun sendNotification(granted: Boolean) { + if (networkConnectivityObserver.networkStatus.first() != NetworkStatus.Available) { + setState { copy(errorDialogMessage = errorMessageProvider.getNetworkError()) } + return + } - private suspend fun getNotificationInfo(): NotificationInfoResponseDto? { - return notificationRepository.getNotificationInfo().getOrElse { - _draftAlarmChangeState.value = NotificationChangeState.Failure(errorMessageProvider.getTemporaryError()) - null + buildAndSendNotification { info, token -> + SendNotificationRequestDto( + isDiaryAlarm = granted, + isDraftAlarm = info.isDraftAlarm, + isReplyAlarm = granted, + time = info.time, + fcmToken = token, + ) } } - 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 enableDraftAlarm() { + if (networkConnectivityObserver.networkStatus.first() != NetworkStatus.Available) { + setState { copy(errorDialogMessage = errorMessageProvider.getNetworkError()) } + return + } - private suspend fun sendDraftAlarmRequest(request: SendNotificationRequestDto) { - withContext(Dispatchers.IO) { - notificationRepository.sendNotification(request) + buildAndSendNotification { info, token -> + SendNotificationRequestDto( + isDiaryAlarm = info.isDiaryAlarm, + isDraftAlarm = true, + isReplyAlarm = info.isReplyAlarm, + time = info.time, + fcmToken = token, + ) }.fold( onSuccess = { - _draftAlarmEnableToast.value = true - _draftAlarmChangeState.value = NotificationChangeState.Success(it) + setState { copy(showDraftNotificationPopup = false, showDraftNotificationToast = true) } }, onFailure = { - _draftAlarmChangeState.value = - NotificationChangeState.Failure(errorMessageProvider.getTemporaryError()) + setState { copy(errorDialogMessage = errorMessageProvider.getTemporaryError()) } }, ) } - fun resetDraftAlarmEnableToast() { - _draftAlarmEnableToast.value = false + private fun updateDraftPopupFlag(flag: Boolean) { + draftRepository.setIsFirstUse(flag) + setState { copy(showDraftNotificationPopup = flag) } } - fun updateShowInAppReviewPopup(state: Boolean) { - reviewRepository.setShouldShowPopup(state) - _showInAppReviewPopup.value = state + private fun updateInAppReviewFlag(newValue: Boolean) { + reviewRepository.setShouldShowPopup(newValue) + setState { copy(showInAppReviewPopup = newValue) } } - 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/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, - ) - } -} 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..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.domain.model.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 492ea5f2..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.domain.model.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 be1af5f9..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.domain.model.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 5b931081..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.domain.model.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..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.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 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 new file mode 100644 index 00000000..58258707 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/ui/type/DailyCloverType.kt @@ -0,0 +1,45 @@ +package com.sopt.clody.presentation.ui.type + +import androidx.annotation.DrawableRes +import com.sopt.clody.R +import com.sopt.clody.domain.model.CalendarMonthlyInfo +import com.sopt.clody.domain.type.ReplyStatus + +/** + * DiaryData를 기반으로 해당 날짜에 보여줄 클로버 아이콘 타입을 반환. + * + * - 오늘이고 일기가 없으면 👉 [ENABLED_DIARY] + * - 오늘이고 일기와 읽지 않은 답장이 있으면 👉 [WAITING_REPLY] + * - 임시저장이 존재하면 👉 [DRAFT_SAVED] + * - 임시저장이 만료되었으면 👉 [DISABLED_REPLY] + * - 일기가 있고 답장이 없거나 읽지 않았으면 👉 [UNGIVEN_CLOVER] + * - 일기 수에 따라 👉 [BOTTOM_CLOVER], [MID_CLOVER], [TOP_CLOVER] 구분 + * - 이 외의 경우 기본값 👉 [UNGIVEN_CLOVER] + */ + +enum class DailyCloverType(@DrawableRes val iconRes: Int) { + 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), + ; + + companion object { + fun getType(info: CalendarMonthlyInfo.CalendarDailyInfo): DailyCloverType { + return when { + info.replyStatus == ReplyStatus.HAS_DRAFT -> DRAFT_SAVED + 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 + 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 new file mode 100644 index 00000000..aced5045 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/ui/type/DailyStateButtonType.kt @@ -0,0 +1,27 @@ +package com.sopt.clody.presentation.ui.type + +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 getType( + calendarDailyInfo: CalendarMonthlyInfo.CalendarDailyInfo, + dailyDiaryInfo: DailyDiaryInfo, + ): DailyStateButtonType { + return when { + calendarDailyInfo.isDeleted && calendarDailyInfo.diaryCount > 0 -> REPLY_DISABLED + dailyDiaryInfo.isDraft -> DRAFT_ENABLED + 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 + } + } + } +} 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/DiaryCloverType.kt deleted file mode 100644 index bb02178a..00000000 --- a/app/src/main/java/com/sopt/clody/presentation/ui/type/DiaryCloverType.kt +++ /dev/null @@ -1,58 +0,0 @@ -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를 기반으로 해당 날짜에 보여줄 클로버 아이콘 타입을 반환. - * - * - 오늘이고 일기가 없으면 👉 [TODAY_UNWRITTEN] - * - 오늘이고 일기와 읽지 않은 답장이 있으면 👉 [TODAY_WRITTEN] - * - 임시저장이 존재하면 👉 [DRAFT_SAVED] - * - 임시저장이 만료되었으면 👉 [EXPIRED_WRITTEN] - * - 일기가 있고 답장이 없거나 읽지 않았으면 👉 [UNGIVEN_CLOVER] - * - 일기 수에 따라 👉 [BOTTOM_CLOVER], [MID_CLOVER], [TOP_CLOVER] 구분 - * - 이 외의 경우 기본값 👉 [UNGIVEN_CLOVER] - */ - -enum class DiaryCloverType(@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), - 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 { - fun getCalendarCloverType( - diaryData: MonthlyCalendarResponseDto.Diary, - isToday: Boolean, - ): DiaryCloverType { - 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 - - return when { - hasDraft -> DRAFT_SAVED - isToday && noDiary -> TODAY_UNWRITTEN - isToday && hasUnreadOrNoReply -> TODAY_WRITTEN - draftExpired -> EXPIRED_WRITTEN - hasDiary && hasUnreadOrNoReply -> UNGIVEN_CLOVER - count in 1..2 -> BOTTOM_CLOVER - count in 3..4 -> MID_CLOVER - count >= 5 -> TOP_CLOVER - else -> UNGIVEN_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..e0bc4313 --- /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, +} 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..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 @@ -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) } 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..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.domain.model.ReplyStatus +import com.sopt.clody.domain.type.ReplyStatus import kotlinx.serialization.Serializable /** 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