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