diff --git a/core/design-system/src/commonMain/composeResources/drawable/ic_home_selected.png b/core/design-system/src/commonMain/composeResources/drawable/ic_home_selected.png index 2326a609..602b71c3 100644 Binary files a/core/design-system/src/commonMain/composeResources/drawable/ic_home_selected.png and b/core/design-system/src/commonMain/composeResources/drawable/ic_home_selected.png differ diff --git a/core/design-system/src/commonMain/composeResources/drawable/ic_home_unselected.png b/core/design-system/src/commonMain/composeResources/drawable/ic_home_unselected.png index 19038996..9d28a4f7 100644 Binary files a/core/design-system/src/commonMain/composeResources/drawable/ic_home_unselected.png and b/core/design-system/src/commonMain/composeResources/drawable/ic_home_unselected.png differ diff --git a/core/design-system/src/commonMain/kotlin/com/ondot/designsystem/extensions/OnDotTextStyle.kt b/core/design-system/src/commonMain/kotlin/com/ondot/designsystem/extensions/OnDotTextStyle.kt index 568cd7ce..255850f1 100644 --- a/core/design-system/src/commonMain/kotlin/com/ondot/designsystem/extensions/OnDotTextStyle.kt +++ b/core/design-system/src/commonMain/kotlin/com/ondot/designsystem/extensions/OnDotTextStyle.kt @@ -29,5 +29,6 @@ fun OnDotTextStyle.toTextStyle(): TextStyle { OnDotTextStyle.BodySmallR1 -> typo.bodySmallR1 OnDotTextStyle.BodySmallR2 -> typo.bodySmallR2 OnDotTextStyle.BodySmallR3 -> typo.bodySmallR3 + OnDotTextStyle.BodySmallM -> typo.bodySmallM } } diff --git a/core/design-system/src/commonMain/kotlin/com/ondot/designsystem/theme/OnDotTypo.kt b/core/design-system/src/commonMain/kotlin/com/ondot/designsystem/theme/OnDotTypo.kt index f375f6a4..87c84068 100644 --- a/core/design-system/src/commonMain/kotlin/com/ondot/designsystem/theme/OnDotTypo.kt +++ b/core/design-system/src/commonMain/kotlin/com/ondot/designsystem/theme/OnDotTypo.kt @@ -51,6 +51,7 @@ data class OnDotTypography( val bodySmallR1: TextStyle, val bodySmallR2: TextStyle, val bodySmallR3: TextStyle, + val bodySmallM: TextStyle, ) @Composable @@ -79,5 +80,6 @@ fun OnDotTypo(): OnDotTypography { bodySmallR1 = TextStyle(fontFamily = fontFamily, fontWeight = FontWeight.Normal, fontSize = 12.sp, lineHeight = lh(12)), bodySmallR2 = TextStyle(fontFamily = fontFamily, fontWeight = FontWeight.Normal, fontSize = 11.sp, lineHeight = lh(11)), bodySmallR3 = TextStyle(fontFamily = fontFamily, fontWeight = FontWeight.Normal, fontSize = 8.sp, lineHeight = lh(8)), + bodySmallM = TextStyle(fontFamily = fontFamily, fontWeight = FontWeight.Medium, fontSize = 9.sp, lineHeight = lh(9)), ) } diff --git a/core/design-system/src/commonMain/kotlin/com/ondot/designsystem/theme/String.kt b/core/design-system/src/commonMain/kotlin/com/ondot/designsystem/theme/String.kt index 1c908368..aa71939e 100644 --- a/core/design-system/src/commonMain/kotlin/com/ondot/designsystem/theme/String.kt +++ b/core/design-system/src/commonMain/kotlin/com/ondot/designsystem/theme/String.kt @@ -33,11 +33,13 @@ const val WORD_DELETE = "삭제" const val WORD_YES = "예" const val WORD_NO = "아니요" const val WORD_PREPARATION = "준비" +const val WORD_PREPARATION_ITEM = "준비물" const val WORD_DEPARTURE = "출발" const val WORD_DELETE_ACCOUNT_ACTION = "탈퇴하기" const val WORD_RESTORE_ACTION = "되돌리기" const val WORD_HOME = "집" const val WORD_EVERYTIME = "에브리타임" +const val WORD_TODAY = "오늘" // 알람 카테고리 const val CATEGORY_GENERAL = "기본" @@ -255,3 +257,5 @@ const val EVERYTIME_ROUTE_LOADING_LABEL = "수업 일정에 맞춰\n알람을 // Calendar const val CALENDAR_EMPTY_SCHEDULES_GUIDE = "등록된 알람이 없습니다." + +fun calendarOverflowScheduleCount(count: Int) = "+$count" diff --git a/core/ui/src/androidMain/kotlin/com/ondot/ui/util/SystemTrace.android.kt b/core/ui/src/androidMain/kotlin/com/ondot/ui/util/SystemTrace.android.kt new file mode 100644 index 00000000..f7521d5b --- /dev/null +++ b/core/ui/src/androidMain/kotlin/com/ondot/ui/util/SystemTrace.android.kt @@ -0,0 +1,20 @@ +package com.ondot.ui.util + +import android.os.Trace +import com.dh.core.ui.BuildConfig + +actual inline fun systemTrace( + name: String, + block: () -> T, +): T { + if (!BuildConfig.DEBUG) { + return block() + } + + Trace.beginSection(name) + return try { + block() + } finally { + Trace.endSection() + } +} diff --git a/core/ui/src/commonMain/kotlin/com/ondot/ui/util/SystemTrace.kt b/core/ui/src/commonMain/kotlin/com/ondot/ui/util/SystemTrace.kt new file mode 100644 index 00000000..ea7cc8e0 --- /dev/null +++ b/core/ui/src/commonMain/kotlin/com/ondot/ui/util/SystemTrace.kt @@ -0,0 +1,6 @@ +package com.ondot.ui.util + +expect inline fun systemTrace( + name: String, + block: () -> T, +): T diff --git a/core/ui/src/commonMain/kotlin/com/ondot/ui/util/TraceLayoutModifier.kt b/core/ui/src/commonMain/kotlin/com/ondot/ui/util/TraceLayoutModifier.kt new file mode 100644 index 00000000..d8790edd --- /dev/null +++ b/core/ui/src/commonMain/kotlin/com/ondot/ui/util/TraceLayoutModifier.kt @@ -0,0 +1,34 @@ +package com.ondot.ui.util + +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.layout.layout + +/** + * Compose의 Layout 단계에서 + * measure(크기 계산), layout(위치 계산) 각각의 시간이 얼마나 걸리는지를 System Trace에 기록하는 함수 + * */ +fun Modifier.traceLayout(name: String): Modifier = + layout { measurable, constraints -> + val placeable = + systemTrace("measure:$name") { + measurable.measure(constraints) + } + + systemTrace("layout:$name") { + layout(placeable.width, placeable.height) { + placeable.placeRelative(0, 0) + } + } + } + +/** + * Compose의 Draw 단계에서 + * draw(그리기) 시간이 얼마나 걸리는지를 System Trace에 기록하는 함수 + * */ +fun Modifier.traceDraw(name: String): Modifier = + drawWithContent { + systemTrace("draw:$name") { + drawContent() + } + } diff --git a/core/ui/src/iosMain/kotlin/com/ondot/ui/util/SystemTrace.ios.kt b/core/ui/src/iosMain/kotlin/com/ondot/ui/util/SystemTrace.ios.kt new file mode 100644 index 00000000..afe0e9a9 --- /dev/null +++ b/core/ui/src/iosMain/kotlin/com/ondot/ui/util/SystemTrace.ios.kt @@ -0,0 +1,6 @@ +package com.ondot.ui.util + +actual inline fun systemTrace( + name: String, + block: () -> T, +): T = block() diff --git a/core/util/src/commonMain/kotlin/com/ondot/util/DateTimeFormatter.kt b/core/util/src/commonMain/kotlin/com/ondot/util/DateTimeFormatter.kt index b942cb2b..bbabc6ac 100644 --- a/core/util/src/commonMain/kotlin/com/ondot/util/DateTimeFormatter.kt +++ b/core/util/src/commonMain/kotlin/com/ondot/util/DateTimeFormatter.kt @@ -156,6 +156,9 @@ object DateTimeFormatter { /** LocalDate 06월 13일 화요일 형태로 포맷 */ fun LocalDate.formatKoreanMonthDay(): String = "${month.number.pad2()}월 ${day.pad2()}일 ${this.extractDayOfWeek()}" + /** LocalDate 6월 13일 화요일 형태로 포맷 */ + fun LocalDate.formatKoreanMonthDayWithoutPad(): String = "${month.number}월 ${day}일 ${this.extractDayOfWeek()}" + /** iso 문자열을 days 만큼 뒤로 미룸 */ fun plusDays( iso: String, diff --git a/data/src/commonMain/kotlin/com/ondot/data/model/response/calendar/CalendarRangeResponse.kt b/data/src/commonMain/kotlin/com/ondot/data/model/response/calendar/CalendarRangeResponse.kt index f56fd662..bd6a9a65 100644 --- a/data/src/commonMain/kotlin/com/ondot/data/model/response/calendar/CalendarRangeResponse.kt +++ b/data/src/commonMain/kotlin/com/ondot/data/model/response/calendar/CalendarRangeResponse.kt @@ -29,4 +29,6 @@ data class CalendarScheduleSummaryResponse( val isRepeat: Boolean, @SerialName("appointmentAt") val appointmentAt: String, + @SerialName("hasActiveAlarm") + val hasActiveAlarm: Boolean, ) diff --git a/data/src/commonMain/kotlin/com/ondot/data/model/response/calendar/mapper/CalendarResponseMapper.kt b/data/src/commonMain/kotlin/com/ondot/data/model/response/calendar/mapper/CalendarResponseMapper.kt index 5b8cade5..a95cbbab 100644 --- a/data/src/commonMain/kotlin/com/ondot/data/model/response/calendar/mapper/CalendarResponseMapper.kt +++ b/data/src/commonMain/kotlin/com/ondot/data/model/response/calendar/mapper/CalendarResponseMapper.kt @@ -19,6 +19,8 @@ fun CalendarRangeResponse.toDomain(): List = CalendarDateScheduleSummaryItem( scheduleId = it.scheduleId, title = it.title, + isRepeat = it.isRepeat, + hasActiveAlarm = it.hasActiveAlarm, ) }, ) diff --git a/domain/src/commonMain/kotlin/com/ondot/domain/model/calendar/CalendarDateScheduleSummary.kt b/domain/src/commonMain/kotlin/com/ondot/domain/model/calendar/CalendarDateScheduleSummary.kt index ee9e6c39..6feb4427 100644 --- a/domain/src/commonMain/kotlin/com/ondot/domain/model/calendar/CalendarDateScheduleSummary.kt +++ b/domain/src/commonMain/kotlin/com/ondot/domain/model/calendar/CalendarDateScheduleSummary.kt @@ -10,4 +10,6 @@ data class CalendarDateScheduleSummary( data class CalendarDateScheduleSummaryItem( val scheduleId: Long, val title: String, + val isRepeat: Boolean, + val hasActiveAlarm: Boolean, ) diff --git a/domain/src/commonMain/kotlin/com/ondot/domain/model/enums/OnDotTextStyle.kt b/domain/src/commonMain/kotlin/com/ondot/domain/model/enums/OnDotTextStyle.kt index a132057e..08042661 100644 --- a/domain/src/commonMain/kotlin/com/ondot/domain/model/enums/OnDotTextStyle.kt +++ b/domain/src/commonMain/kotlin/com/ondot/domain/model/enums/OnDotTextStyle.kt @@ -20,4 +20,5 @@ enum class OnDotTextStyle { BodySmallR1, BodySmallR2, BodySmallR3, + BodySmallM, } diff --git a/feature/calendar/src/commonMain/kotlin/com/ondot/calendar/contract/CalendarIntent.kt b/feature/calendar/src/commonMain/kotlin/com/ondot/calendar/contract/CalendarIntent.kt index 68c2d705..ab6dfc41 100644 --- a/feature/calendar/src/commonMain/kotlin/com/ondot/calendar/contract/CalendarIntent.kt +++ b/feature/calendar/src/commonMain/kotlin/com/ondot/calendar/contract/CalendarIntent.kt @@ -12,6 +12,8 @@ sealed interface CalendarIntent : Intent { data object MoveToNextMonth : CalendarIntent + data object MoveToToday : CalendarIntent + data class ToggleAlarm( val scheduleId: Long, val enabled: Boolean, diff --git a/feature/calendar/src/commonMain/kotlin/com/ondot/calendar/contract/CalendarScheduleItemUiModel.kt b/feature/calendar/src/commonMain/kotlin/com/ondot/calendar/contract/CalendarScheduleItemUiModel.kt index f23066d1..cd85a184 100644 --- a/feature/calendar/src/commonMain/kotlin/com/ondot/calendar/contract/CalendarScheduleItemUiModel.kt +++ b/feature/calendar/src/commonMain/kotlin/com/ondot/calendar/contract/CalendarScheduleItemUiModel.kt @@ -12,4 +12,5 @@ data class CalendarScheduleItemUiModel( val isAlarmEnabled: Boolean, val isPast: Boolean, val repeatDays: List, + val preparationNote: String = "", ) diff --git a/feature/calendar/src/commonMain/kotlin/com/ondot/calendar/contract/CalendarUiState.kt b/feature/calendar/src/commonMain/kotlin/com/ondot/calendar/contract/CalendarUiState.kt index c796750f..b6b588ac 100644 --- a/feature/calendar/src/commonMain/kotlin/com/ondot/calendar/contract/CalendarUiState.kt +++ b/feature/calendar/src/commonMain/kotlin/com/ondot/calendar/contract/CalendarUiState.kt @@ -45,6 +45,8 @@ data class CalendarMonth( data class CalendarScheduleMarker( val scheduleId: Long, val title: String, + val isRepeat: Boolean, + val hasActiveAlarm: Boolean, ) @Immutable @@ -156,6 +158,7 @@ fun Schedule.toCalendarScheduleItemUiModel( isAlarmEnabled = hasActiveAlarm, isPast = isPast, repeatDays = repeatDays, + preparationNote = preparationNote, ) } diff --git a/feature/calendar/src/commonMain/kotlin/com/ondot/calendar/contract/CalendarViewModel.kt b/feature/calendar/src/commonMain/kotlin/com/ondot/calendar/contract/CalendarViewModel.kt index 0694d233..af78c0c8 100644 --- a/feature/calendar/src/commonMain/kotlin/com/ondot/calendar/contract/CalendarViewModel.kt +++ b/feature/calendar/src/commonMain/kotlin/com/ondot/calendar/contract/CalendarViewModel.kt @@ -19,9 +19,13 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.datetime.DatePeriod import kotlinx.datetime.LocalDate +import kotlinx.datetime.TimeZone import kotlinx.datetime.number import kotlinx.datetime.plus +import kotlinx.datetime.toLocalDateTime import kotlin.coroutines.cancellation.CancellationException +import kotlin.time.Clock +import kotlin.time.ExperimentalTime class CalendarViewModel( private val calendarRepository: CalendarRepository, @@ -84,6 +88,21 @@ class CalendarViewModel( loadSchedulesFor(nextSelectedDate) } + CalendarIntent.MoveToToday -> { + val today = today() + val todayMonth = CalendarMonth(today.year, today.month.number) + + reduce { + copy( + currentMonth = todayMonth, + selectedDate = today, + ) + } + + getScheduleMarkersInRange(today) + loadSchedulesFor(today) + } + is CalendarIntent.ToggleAlarm -> { toggleAlarm( scheduleId = intent.scheduleId, @@ -141,7 +160,12 @@ class CalendarViewModel( summaries.associate { summary -> summary.date to summary.schedules.map { schedule -> - CalendarScheduleMarker(scheduleId = schedule.scheduleId, title = schedule.title) + CalendarScheduleMarker( + scheduleId = schedule.scheduleId, + title = schedule.title, + isRepeat = schedule.isRepeat, + hasActiveAlarm = schedule.hasActiveAlarm, + ) } } @@ -513,3 +537,10 @@ private fun daysInMonth( } private fun isLeapYear(year: Int): Boolean = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) + +@OptIn(ExperimentalTime::class) +private fun today(): LocalDate = + Clock.System + .now() + .toLocalDateTime(TimeZone.currentSystemDefault()) + .date diff --git a/feature/calendar/src/commonMain/kotlin/com/ondot/calendar/ui/CalendarScreen.kt b/feature/calendar/src/commonMain/kotlin/com/ondot/calendar/ui/CalendarScreen.kt index 925b171f..52991a01 100644 --- a/feature/calendar/src/commonMain/kotlin/com/ondot/calendar/ui/CalendarScreen.kt +++ b/feature/calendar/src/commonMain/kotlin/com/ondot/calendar/ui/CalendarScreen.kt @@ -47,6 +47,7 @@ import com.dh.ondot.presentation.ui.theme.DELETE_REPEAT_SCHEDULE_TITLE import com.dh.ondot.presentation.ui.theme.WORD_DELETE import com.ondot.calendar.contract.CalendarIntent import com.ondot.calendar.contract.CalendarInteractionState +import com.ondot.calendar.contract.CalendarMonth import com.ondot.calendar.contract.CalendarSheetAnchor import com.ondot.calendar.contract.CalendarSheetState import com.ondot.calendar.contract.CalendarSideEffect @@ -60,6 +61,7 @@ import com.ondot.calendar.ui.component.CalendarBottomSheet import com.ondot.calendar.ui.component.CalendarMonthGrid import com.ondot.calendar.ui.component.CalendarTopBar import com.ondot.calendar.ui.component.CalendarWeekHeader +import com.ondot.calendar.ui.component.TodayFAB import com.ondot.designsystem.components.OnDotDialog import com.ondot.designsystem.components.OnDotText import com.ondot.designsystem.theme.OnDotColor.Gray800 @@ -70,10 +72,15 @@ import com.ondot.domain.model.enums.OnDotTextStyle import com.ondot.ui.util.ToastManager import kotlinx.coroutines.launch import kotlinx.datetime.LocalDate +import kotlinx.datetime.TimeZone +import kotlinx.datetime.number +import kotlinx.datetime.toLocalDateTime import ondot.core.design_system.generated.resources.Res import ondot.core.design_system.generated.resources.ic_plus import org.jetbrains.compose.resources.painterResource import org.koin.compose.viewmodel.koinViewModel +import kotlin.time.Clock +import kotlin.time.ExperimentalTime @Composable fun CalendarRoute( @@ -102,6 +109,7 @@ fun CalendarRoute( onSelectDate = { viewModel.dispatch(CalendarIntent.SelectDate(it)) }, onMoveToPreviousMonth = { viewModel.dispatch(CalendarIntent.MoveToPreviousMonth) }, onMoveToNextMonth = { viewModel.dispatch(CalendarIntent.MoveToNextMonth) }, + onMoveToToday = { viewModel.dispatch(CalendarIntent.MoveToToday) }, onToggleAlarm = { scheduleId, enabled -> viewModel.dispatch(CalendarIntent.ToggleAlarm(scheduleId, enabled)) }, onAddSchedule = navigateToCreateGeneralSchedule, onDelete = { id, isPast -> viewModel.dispatch(CalendarIntent.DeleteScheduleItem(id, isPast)) }, @@ -109,12 +117,14 @@ fun CalendarRoute( ) } +@OptIn(ExperimentalTime::class) @Composable private fun CalendarScreen( uiState: CalendarUiState, onSelectDate: (LocalDate) -> Unit, onMoveToPreviousMonth: () -> Unit, onMoveToNextMonth: () -> Unit, + onMoveToToday: () -> Unit, onToggleAlarm: (Long, Boolean) -> Unit, onAddSchedule: () -> Unit, onDelete: (Long, Boolean) -> Unit, @@ -149,6 +159,20 @@ private fun CalendarScreen( var scheduleIdPendingDelete by remember { mutableStateOf(null) } + // TodayFAB 렌더링을 위한 상태 변수 + val today = + remember { + Clock.System + .now() + .toLocalDateTime(TimeZone.currentSystemDefault()) + .date + } + val todayMonth = + remember(today) { + CalendarMonth(today.year, today.month.number) + } + val shouldShowTodayFab = uiState.currentMonth != todayMonth + BoxWithConstraints( modifier = Modifier @@ -300,7 +324,7 @@ private fun CalendarScreen( Spacer(Modifier.height(20.dp)) CalendarWeekHeader( - modifier = Modifier.padding(horizontal = 22.dp), + modifier = Modifier.padding(horizontal = 16.dp), ) } @@ -309,7 +333,7 @@ private fun CalendarScreen( Modifier .fillMaxWidth() .height(calendarBodyHeight) - .padding(horizontal = 22.dp) + .padding(horizontal = 16.dp) .clip(RoundedCornerShape(bottomStart = 24.dp, bottomEnd = 24.dp)), ) { AnimatedContent( @@ -384,6 +408,7 @@ private fun CalendarScreen( onDelete = onDelete, onClickSchedule = onClickSchedule, onShowScheduleDeleteDialog = { scheduleIdPendingDelete = it }, + traceName = "CalendarBottomSheetBase", ) } } @@ -417,6 +442,7 @@ private fun CalendarScreen( onDelete = onDelete, onClickSchedule = onClickSchedule, onShowScheduleDeleteDialog = { scheduleIdPendingDelete = it }, + traceName = "CalendarBottomSheetOverlay", ) } @@ -428,6 +454,16 @@ private fun CalendarScreen( onClick = onAddSchedule, ) + if (shouldShowTodayFab) { + TodayFAB( + modifier = + Modifier + .align(Alignment.BottomCenter) + .padding(bottom = 24.dp), + onClick = onMoveToToday, + ) + } + if (scheduleIdPendingDelete != null) { OnDotDialog( dialogType = DialogType.TWO, diff --git a/feature/calendar/src/commonMain/kotlin/com/ondot/calendar/ui/component/CalendarBottomSheet.kt b/feature/calendar/src/commonMain/kotlin/com/ondot/calendar/ui/component/CalendarBottomSheet.kt index 3c1df004..d632e35f 100644 --- a/feature/calendar/src/commonMain/kotlin/com/ondot/calendar/ui/component/CalendarBottomSheet.kt +++ b/feature/calendar/src/commonMain/kotlin/com/ondot/calendar/ui/component/CalendarBottomSheet.kt @@ -24,8 +24,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.dh.ondot.presentation.ui.theme.CALENDAR_EMPTY_SCHEDULES_GUIDE +import com.dh.ondot.presentation.ui.theme.WORD_PREPARATION_ITEM import com.ondot.calendar.contract.CalendarScheduleItemUiModel import com.ondot.designsystem.components.OnDotSwitch import com.ondot.designsystem.components.OnDotText @@ -34,11 +36,15 @@ import com.ondot.designsystem.theme.OnDotColor.Gray0 import com.ondot.designsystem.theme.OnDotColor.Gray200 import com.ondot.designsystem.theme.OnDotColor.Gray300 import com.ondot.designsystem.theme.OnDotColor.Gray400 +import com.ondot.designsystem.theme.OnDotColor.Gray500 import com.ondot.designsystem.theme.OnDotColor.Gray700 import com.ondot.designsystem.theme.OnDotColor.Green500 +import com.ondot.designsystem.theme.OnDotColor.Green600 import com.ondot.domain.model.enums.OnDotTextStyle import com.ondot.ui.util.noRippleClickable -import com.ondot.util.DateTimeFormatter.formatKoreanMonthDay +import com.ondot.ui.util.traceDraw +import com.ondot.ui.util.traceLayout +import com.ondot.util.DateTimeFormatter.formatKoreanMonthDayWithoutPad import kotlinx.datetime.LocalDate import ondot.core.design_system.generated.resources.Res import ondot.core.design_system.generated.resources.ic_no_clock @@ -55,9 +61,13 @@ fun CalendarBottomSheet( onDelete: (Long, Boolean) -> Unit = { _, _ -> }, // 반복 일정이 아닌 경우 그냥 삭제 onClickSchedule: (Long) -> Unit = {}, onShowScheduleDeleteDialog: (Long) -> Unit = {}, // 반복 일정인 경우 삭제 다이얼로그 띄우기 + traceName: String = "CalendarBottomSheet", ) { Surface( - modifier = modifier, + modifier = + modifier + .traceLayout(traceName) + .traceDraw(traceName), color = Gray700, shadowElevation = 10.dp, ) { @@ -79,7 +89,7 @@ fun CalendarBottomSheet( ) OnDotText( - text = selectedDate.formatKoreanMonthDay(), + text = selectedDate.formatKoreanMonthDayWithoutPad(), color = Gray0, style = OnDotTextStyle.TitleSmallSB, modifier = @@ -257,5 +267,40 @@ private fun CalendarScheduleListItem( ) } } + + if (item.preparationNote.isNotBlank()) { + Spacer(Modifier.height(12.dp)) + PreparationNoteItem(item.preparationNote) + } + } +} + +@Composable +private fun PreparationNoteItem(note: String) { + Column( + modifier = + Modifier + .fillMaxWidth() + .height(65.dp) + .background(Gray500, RoundedCornerShape(8.dp)) + .padding(12.dp), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Center, + ) { + OnDotText( + text = WORD_PREPARATION_ITEM, + style = OnDotTextStyle.BodySmallR1, + color = Gray200, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + + Spacer(Modifier.height(4.dp)) + + OnDotText( + text = note, + style = OnDotTextStyle.BodyMediumR, + color = Green600, + ) } } diff --git a/feature/calendar/src/commonMain/kotlin/com/ondot/calendar/ui/component/CalendarDay.kt b/feature/calendar/src/commonMain/kotlin/com/ondot/calendar/ui/component/CalendarDay.kt index b94da8d7..71799d02 100644 --- a/feature/calendar/src/commonMain/kotlin/com/ondot/calendar/ui/component/CalendarDay.kt +++ b/feature/calendar/src/commonMain/kotlin/com/ondot/calendar/ui/component/CalendarDay.kt @@ -17,10 +17,14 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import com.dh.ondot.presentation.ui.theme.calendarOverflowScheduleCount import com.ondot.calendar.contract.CalendarDayCell +import com.ondot.calendar.contract.CalendarScheduleMarker import com.ondot.designsystem.components.OnDotText import com.ondot.designsystem.theme.OnDotColor.Gray100 +import com.ondot.designsystem.theme.OnDotColor.Gray300 import com.ondot.designsystem.theme.OnDotColor.Gray400 import com.ondot.designsystem.theme.OnDotColor.Gray600 import com.ondot.designsystem.theme.OnDotColor.Green500 @@ -95,12 +99,17 @@ fun CalendarDay( BoxWithConstraints( contentAlignment = Alignment.TopCenter, ) { - val chipHeight = 14.dp + val chipHeight = 16.dp val chipSpacing = 3.dp val maxChipCount = ((maxHeight + chipSpacing) / (chipHeight + chipSpacing)) .toInt() .coerceAtLeast(1) + val visibleScheduleChips = + resolveVisibleScheduleChips( + markers = cell.markers, + maxSlotCount = maxChipCount, + ) Box( modifier = @@ -117,9 +126,20 @@ fun CalendarDay( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(chipSpacing), ) { - cell.markers.take(maxChipCount).forEach { marker -> + visibleScheduleChips.markers.forEach { marker -> ScheduleChip( text = marker.title, + isRepeat = marker.isRepeat, + hasActiveAlarm = marker.hasActiveAlarm, + ) + } + + if (visibleScheduleChips.overflowCount > 0) { + OnDotText( + text = calendarOverflowScheduleCount(visibleScheduleChips.overflowCount), + style = OnDotTextStyle.BodySmallR2, + color = Gray300, + textAlign = TextAlign.Center, ) } } @@ -156,3 +176,27 @@ fun CalendarDay( } } } + +private data class VisibleScheduleChips( + val markers: List, + val overflowCount: Int, +) + +private fun resolveVisibleScheduleChips( + markers: List, + maxSlotCount: Int, +): VisibleScheduleChips { + if (markers.size <= maxSlotCount) { + return VisibleScheduleChips( + markers = markers, + overflowCount = 0, + ) + } + + val visibleCount = (maxSlotCount - 1).coerceAtLeast(0) + + return VisibleScheduleChips( + markers = markers.take(visibleCount), + overflowCount = markers.size - visibleCount, + ) +} diff --git a/feature/calendar/src/commonMain/kotlin/com/ondot/calendar/ui/component/CalendarMonthGrid.kt b/feature/calendar/src/commonMain/kotlin/com/ondot/calendar/ui/component/CalendarMonthGrid.kt index 410ff19d..01706c7a 100644 --- a/feature/calendar/src/commonMain/kotlin/com/ondot/calendar/ui/component/CalendarMonthGrid.kt +++ b/feature/calendar/src/commonMain/kotlin/com/ondot/calendar/ui/component/CalendarMonthGrid.kt @@ -14,6 +14,8 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.ondot.calendar.contract.CalendarDayCell import com.ondot.designsystem.theme.OnDotColor.Gray700 +import com.ondot.ui.util.traceDraw +import com.ondot.ui.util.traceLayout import kotlinx.datetime.LocalDate import kotlin.collections.chunked import kotlin.collections.forEach @@ -40,7 +42,11 @@ fun CalendarMonthGrid( .coerceAtLeast(0.dp) Column( - modifier = Modifier.fillMaxSize(), + modifier = + Modifier + .fillMaxSize() + .traceLayout("CalendarMonthGrid") + .traceDraw("CalendarMonthGrid"), verticalArrangement = Arrangement.spacedBy(verticalGap), ) { weeks.forEachIndexed { index, week -> @@ -49,7 +55,7 @@ fun CalendarMonthGrid( Modifier .fillMaxWidth() .height(rowHeight), - horizontalArrangement = Arrangement.spacedBy(12.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), ) { week.forEach { cell -> CalendarDay( diff --git a/feature/calendar/src/commonMain/kotlin/com/ondot/calendar/ui/component/ScheduleChip.kt b/feature/calendar/src/commonMain/kotlin/com/ondot/calendar/ui/component/ScheduleChip.kt index 4b1160ba..e049bf5f 100644 --- a/feature/calendar/src/commonMain/kotlin/com/ondot/calendar/ui/component/ScheduleChip.kt +++ b/feature/calendar/src/commonMain/kotlin/com/ondot/calendar/ui/component/ScheduleChip.kt @@ -1,6 +1,7 @@ package com.ondot.calendar.ui.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.height @@ -13,26 +14,54 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.ondot.designsystem.components.OnDotText +import com.ondot.designsystem.theme.OnDotColor.Gray300 import com.ondot.designsystem.theme.OnDotColor.Gray900 +import com.ondot.designsystem.theme.OnDotColor.Green50 import com.ondot.designsystem.theme.OnDotColor.Green500 +import com.ondot.designsystem.theme.OnDotColor.Green800 import com.ondot.domain.model.enums.OnDotTextStyle @Composable -fun ScheduleChip(text: String) { +fun ScheduleChip( + text: String, + isRepeat: Boolean, + hasActiveAlarm: Boolean, +) { + val backgroundColor = + when { + !hasActiveAlarm -> Gray900 + isRepeat -> Green500 + else -> Green50 + } + val textColor = + when { + !hasActiveAlarm -> Gray300 + isRepeat -> Gray900 + else -> Green800 + } + val borderColor = + when { + !hasActiveAlarm -> Gray300 + isRepeat -> Green500 + else -> Green50 + } + val shape = RoundedCornerShape(2.dp) + Box( modifier = Modifier .fillMaxWidth() - .height(14.dp) - .clip(RoundedCornerShape(2.dp)) - .background(Green500) - .padding(horizontal = 2.dp, vertical = 1.dp), + .height(13.dp) + .clip(shape) + .background(backgroundColor) + .border(1.dp, borderColor, shape) + .padding(horizontal = 2.dp), contentAlignment = Alignment.Center, ) { OnDotText( text = text, - style = OnDotTextStyle.BodySmallR3, - color = Gray900, + style = OnDotTextStyle.BodySmallM, + color = textColor, maxLines = 1, overflow = TextOverflow.Ellipsis, ) diff --git a/feature/calendar/src/commonMain/kotlin/com/ondot/calendar/ui/component/TodayFAB.kt b/feature/calendar/src/commonMain/kotlin/com/ondot/calendar/ui/component/TodayFAB.kt new file mode 100644 index 00000000..1b683a05 --- /dev/null +++ b/feature/calendar/src/commonMain/kotlin/com/ondot/calendar/ui/component/TodayFAB.kt @@ -0,0 +1,44 @@ +package com.ondot.calendar.ui.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +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.unit.dp +import com.dh.ondot.presentation.ui.theme.WORD_TODAY +import com.ondot.designsystem.components.OnDotText +import com.ondot.designsystem.theme.OnDotColor.Gray0 +import com.ondot.designsystem.theme.OnDotColor.Gray300 +import com.ondot.designsystem.theme.OnDotColor.Gray900 +import com.ondot.domain.model.enums.OnDotTextStyle + +@Composable +fun TodayFAB( + modifier: Modifier = Modifier, + onClick: () -> Unit, +) { + Box( + modifier = + modifier + .width(52.dp) + .height(30.dp) + .clip(RoundedCornerShape(99.dp)) + .background(Gray0) + .border(1.dp, Gray300, RoundedCornerShape(99.dp)) + .clickable { onClick() }, + contentAlignment = Alignment.Center, + ) { + OnDotText( + text = WORD_TODAY, + style = OnDotTextStyle.BodyLargeR1, + color = Gray900, + ) + } +} diff --git a/feature/general/src/commonMain/kotlin/com/ondot/general/GeneralScheduleViewModel.kt b/feature/general/src/commonMain/kotlin/com/ondot/general/GeneralScheduleViewModel.kt index c5a320b3..a33e149c 100644 --- a/feature/general/src/commonMain/kotlin/com/ondot/general/GeneralScheduleViewModel.kt +++ b/feature/general/src/commonMain/kotlin/com/ondot/general/GeneralScheduleViewModel.kt @@ -219,6 +219,7 @@ class GeneralScheduleViewModel( uiState.value.copy( placePickerState = uiState.value.placePickerState.copy( + isChecked = uiState.value.placePickerState.isChecked && value.isHomeAddressInput(), departurePlaceInput = value, selectedDeparturePlace = null, ), @@ -260,6 +261,7 @@ class GeneralScheduleViewModel( placePickerState = uiState.value.placePickerState.copy( placeList = emptyList(), + isChecked = uiState.value.placePickerState.isChecked && place.isHomeAddress(), departurePlaceInput = place.title, selectedDeparturePlace = place, ), @@ -306,6 +308,18 @@ class GeneralScheduleViewModel( ) } + private fun String.isHomeAddressInput(): Boolean { + val homeAddress = uiState.value.placePickerState.homeAddress + return this == homeAddress.roadAddress || this == homeAddress.title + } + + private fun AddressInfo.isHomeAddress(): Boolean { + val homeAddress = uiState.value.placePickerState.homeAddress + return roadAddress == homeAddress.roadAddress && + latitude == homeAddress.latitude && + longitude == homeAddress.longitude + } + fun updateInitialPlacePicker(value: Boolean) { updateState(uiState.value.copy(isInitialPlacePicker = value)) } diff --git a/feature/main/src/commonMain/kotlin/com/ondot/main/MainScreen.kt b/feature/main/src/commonMain/kotlin/com/ondot/main/MainScreen.kt index 2778bbdd..7c01e73c 100644 --- a/feature/main/src/commonMain/kotlin/com/ondot/main/MainScreen.kt +++ b/feature/main/src/commonMain/kotlin/com/ondot/main/MainScreen.kt @@ -132,7 +132,7 @@ fun BottomNavBar( .fillMaxWidth() .height(if (getPlatform() == ANDROID) 72.dp else 93.dp) .background(Gray800.copy(alpha = 0.8f)), - horizontalArrangement = Arrangement.Absolute.SpaceEvenly, + horizontalArrangement = Arrangement.spacedBy(85.dp, Alignment.CenterHorizontally), verticalAlignment = Alignment.Top, ) { BottomNavItem( diff --git a/gradle.properties b/gradle.properties index e71f80eb..b08963f6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,5 +21,5 @@ target.sdk=36 compile.sdk=36 # Version -version.code=36 -version.name=1.3.0 \ No newline at end of file +version.code=38 +version.name=1.3.1 \ No newline at end of file diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj index 57e22472..cf55ae24 100644 --- a/iosApp/iosApp.xcodeproj/project.pbxproj +++ b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -468,7 +468,7 @@ CODE_SIGN_ENTITLEMENTS = iosApp/iosApp.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 36; + CURRENT_PROJECT_VERSION = 38; DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = ( @@ -491,7 +491,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.3.0; + MARKETING_VERSION = 1.3.1; ONLY_ACTIVE_ARCH = NO; OTHER_SWIFT_FLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = com.dh.ondot.iosApp; @@ -520,7 +520,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 36; + CURRENT_PROJECT_VERSION = 38; DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = Y2Q4TFA96S; @@ -545,7 +545,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.3.0; + MARKETING_VERSION = 1.3.1; ONLY_ACTIVE_ARCH = NO; OTHER_SWIFT_FLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = com.dh.ondot.iosApp;