diff --git a/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/CalendarRoute.kt b/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/CalendarRoute.kt index 5f63d6dcc..fe3db7cb6 100644 --- a/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/CalendarRoute.kt +++ b/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/CalendarRoute.kt @@ -1,18 +1,19 @@ package com.terning.feature.calendar.calendar +import androidx.compose.animation.core.Transition import androidx.compose.animation.core.tween +import androidx.compose.animation.core.updateTransition import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutHorizontally import androidx.compose.animation.slideOutVertically import androidx.compose.animation.togetherWith -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope @@ -20,19 +21,17 @@ import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.terning.core.analytics.EventType import com.terning.core.analytics.LocalTracker import com.terning.core.designsystem.component.topappbar.CalendarTopAppBar +import com.terning.core.designsystem.extension.getWeekIndexContainingSelectedDate import com.terning.core.designsystem.theme.Grey200 -import com.terning.core.designsystem.theme.White import com.terning.feature.calendar.calendar.component.ScreenTransition import com.terning.feature.calendar.calendar.component.WeekDaysHeader -import com.terning.feature.calendar.calendar.model.CalendarModel.Companion.getLocalDateByPage -import com.terning.feature.calendar.calendar.model.CalendarModel.Companion.getYearMonthByPage import com.terning.feature.calendar.calendar.model.CalendarUiState -import com.terning.feature.calendar.calendar.model.LocalPagerState +import com.terning.feature.calendar.calendar.model.DayModel +import com.terning.feature.calendar.calendar.model.TerningCalendarModel import com.terning.feature.calendar.list.CalendarListRoute import com.terning.feature.calendar.month.CalendarMonthRoute import com.terning.feature.calendar.week.CalendarWeekRoute @@ -45,12 +44,10 @@ fun CalendarRoute( modifier: Modifier = Modifier, viewModel: CalendarViewModel = hiltViewModel() ) { - val lifecycleOwner = LocalLifecycleOwner.current - val uiState by viewModel.uiState.collectAsStateWithLifecycle(lifecycleOwner = lifecycleOwner) + val uiState by viewModel.uiState.collectAsStateWithLifecycle() val amplitudeTracker = LocalTracker.current CalendarScreen( - modifier = modifier, uiState = uiState, navigateToAnnouncement = navigateToAnnouncement, onClickNewDate = viewModel::onSelectNewDate, @@ -65,7 +62,8 @@ fun CalendarRoute( ) } viewModel.updateListVisibility(!uiState.isListEnabled) - } + }, + modifier = modifier, ) } @@ -73,8 +71,8 @@ fun CalendarRoute( private fun CalendarScreen( uiState: CalendarUiState, navigateToAnnouncement: (Long) -> Unit, - onClickNewDate: (LocalDate) -> Unit, - updateSelectedDate: (LocalDate) -> Unit, + onClickNewDate: (DayModel) -> Unit, + updateSelectedDate: (DayModel) -> Unit, disableListVisibility: () -> Unit, disableWeekVisibility: () -> Unit, onClickListButton: () -> Unit, @@ -82,6 +80,11 @@ private fun CalendarScreen( ) { val coroutineScope = rememberCoroutineScope() + val calendarListTransition = + updateTransition(!uiState.isListEnabled, label = "calendarListTransition") + val monthWeekTransition = + updateTransition(!uiState.isWeekEnabled, label = "monthWeekTransition") + val pagerState = rememberPagerState( initialPage = uiState.calendarModel.initialPage, pageCount = { uiState.calendarModel.pageCount } @@ -90,95 +93,148 @@ private fun CalendarScreen( LaunchedEffect(key1 = pagerState, key2 = uiState.selectedDate) { snapshotFlow { pagerState.currentPage } .collect { current -> - val date = getLocalDateByPage(current) + val date = uiState.calendarModel.getLocalDateByPage(current) + val month = uiState.calendarModel.getMonthModelByPage(current) val newDate = LocalDate.of( date.year, date.month, - uiState.selectedDate.dayOfMonth.coerceAtMost(date.month.minLength()) + uiState.selectedDate.date.dayOfMonth.coerceAtMost(date.month.minLength()) ) - updateSelectedDate(newDate) + + val currentWeek = newDate.getWeekIndexContainingSelectedDate(month.inDays) + updateSelectedDate(DayModel(newDate, currentWeek)) } } - CompositionLocalProvider( - LocalPagerState provides pagerState + + Column( + modifier = modifier, ) { - Column( - modifier = modifier, - ) { - CalendarTopAppBar( - date = getYearMonthByPage(pagerState.settledPage), - isListExpanded = uiState.isListEnabled, - onListButtonClicked = onClickListButton, - onMonthNavigationButtonClicked = { direction -> - coroutineScope.launch { - pagerState.animateScrollToPage( - page = pagerState.settledPage + direction, - animationSpec = tween(500) - ) - } + CalendarTopAppBar( + date = uiState.calendarModel.getYearMonthByPage(pagerState.settledPage), + isListExpanded = uiState.isListEnabled, + onListButtonClicked = { + if(!calendarListTransition.isRunning) + onClickListButton() + }, + onMonthNavigationButtonClicked = { direction -> + coroutineScope.launch { + pagerState.animateScrollToPage( + page = pagerState.settledPage + direction, + animationSpec = tween(500) + ) } - ) - ScreenTransition( - targetState = !uiState.isListEnabled, - transitionOne = slideInHorizontally { fullWidth -> -fullWidth } togetherWith - slideOutHorizontally { fullWidth -> fullWidth }, - transitionTwo = slideInHorizontally { fullWidth -> fullWidth } togetherWith - slideOutHorizontally { fullWidth -> -fullWidth }, - contentOne = { - Column( - modifier = Modifier - .fillMaxSize() - ) { - WeekDaysHeader() - - HorizontalDivider( - thickness = 1.dp, - color = Grey200 - ) - - ScreenTransition( - targetState = !uiState.isWeekEnabled, - transitionOne = slideInVertically { fullHeight -> -fullHeight } togetherWith - slideOutVertically { fullHeight -> fullHeight }, - transitionTwo = slideInVertically { fullHeight -> fullHeight } togetherWith - slideOutVertically { fullHeight -> -fullHeight }, - contentOne = { - CalendarMonthRoute( - selectedDate = uiState.selectedDate, - updateSelectedDate = { newDate -> - if (!pagerState.isScrollInProgress) - onClickNewDate(newDate) - }, - modifier = Modifier - .fillMaxSize() - .background(White), - ) - }, - contentTwo = { - CalendarWeekRoute( - calendarUiState = uiState, - modifier = Modifier - .fillMaxSize(), - navigateUp = disableWeekVisibility, - navigateToAnnouncement = navigateToAnnouncement, - updateSelectedDate = onClickNewDate - ) - } - ) - } - }, - contentTwo = { - CalendarListRoute( - navigateToAnnouncement = navigateToAnnouncement, - navigateUp = disableListVisibility, - modifier = Modifier - .fillMaxSize() + } + ) + + CalendarListTransition( + transition = calendarListTransition, + calendarModel = uiState.calendarModel, + pagerState = pagerState, + onNavigateToAnnouncement = navigateToAnnouncement, + onNavigateUpToCalendar = disableListVisibility, + calendarContent = { + Column( + modifier = Modifier + .fillMaxSize() + ) { + WeekDaysHeader() + + HorizontalDivider( + thickness = 1.dp, + color = Grey200 ) + + MonthWeekTransition( + transition = monthWeekTransition, + selectedDate = uiState.selectedDate, + calendarModel = uiState.calendarModel, + pagerState = pagerState, + onSelectDate = { newDate -> onClickNewDate(newDate) }, + onNavigateToAnnouncement = navigateToAnnouncement, + onNavigateUpToMonth = disableWeekVisibility + ) + } + } + ) + } +} + + +/** 달력 <-> 목록 전환 컴포저블 */ +@Composable +private fun CalendarListTransition( + transition: Transition, + calendarModel: TerningCalendarModel, + pagerState: PagerState, + onNavigateToAnnouncement: (Long) -> Unit, + onNavigateUpToCalendar: () -> Unit, + calendarContent: @Composable () -> Unit, +) { + ScreenTransition( + transition = transition, + transitionOne = slideInHorizontally { fullWidth -> -fullWidth } togetherWith + slideOutHorizontally { fullWidth -> fullWidth }, + transitionTwo = slideInHorizontally { fullWidth -> fullWidth } togetherWith + slideOutHorizontally { fullWidth -> -fullWidth }, + contentOne = { + calendarContent() + }, + contentTwo = { + CalendarListRoute( + calendarModel = calendarModel, + navigateToAnnouncement = onNavigateToAnnouncement, + navigateUp = onNavigateUpToCalendar, + pagerState = pagerState, + modifier = Modifier + .fillMaxSize() + ) + }, + ) +} + +/**월간 <-> 주간 전환 컴포저블*/ +@Composable +private fun MonthWeekTransition( + transition: Transition, + selectedDate: DayModel, + calendarModel: TerningCalendarModel, + pagerState: PagerState, + onSelectDate: (DayModel) -> Unit, + onNavigateToAnnouncement: (Long) -> Unit, + onNavigateUpToMonth: () -> Unit, +) { + ScreenTransition( + transition = transition, + transitionOne = slideInVertically { fullHeight -> -fullHeight } togetherWith + slideOutVertically { fullHeight -> fullHeight }, + transitionTwo = slideInVertically { fullHeight -> fullHeight } togetherWith + slideOutVertically { fullHeight -> -fullHeight }, + contentOne = { + CalendarMonthRoute( + selectedDate = selectedDate, + updateSelectedDate = { newDate -> + if (!pagerState.isScrollInProgress) + onSelectDate(newDate) }, + pagerState = pagerState, + calendarModel = calendarModel + ) + }, + contentTwo = { + CalendarWeekRoute( + modifier = Modifier + .fillMaxSize(), + navigateUp = onNavigateUpToMonth, + navigateToAnnouncement = onNavigateToAnnouncement, + updateSelectedDate = onSelectDate, + selectedDate = selectedDate, + calendarModel = calendarModel, + pagerState = pagerState, ) } - } + ) } + diff --git a/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/CalendarViewModel.kt b/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/CalendarViewModel.kt index 5280ae0af..b8787976b 100644 --- a/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/CalendarViewModel.kt +++ b/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/CalendarViewModel.kt @@ -1,14 +1,12 @@ package com.terning.feature.calendar.calendar import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import com.terning.feature.calendar.calendar.model.CalendarUiState +import com.terning.feature.calendar.calendar.model.DayModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import java.time.LocalDate import javax.inject.Inject @HiltViewModel @@ -16,13 +14,9 @@ class CalendarViewModel @Inject constructor() : ViewModel() { private var _uiState: MutableStateFlow = MutableStateFlow(CalendarUiState()) val uiState get() = _uiState.asStateFlow() - fun onSelectNewDate(selectedDate: LocalDate) = viewModelScope.launch { - if (_uiState.value.selectedDate == selectedDate) { - _uiState.update { currentState -> - currentState.copy( - isWeekEnabled = !_uiState.value.isWeekEnabled - ) - } + fun onSelectNewDate(selectedDate: DayModel) { + if (_uiState.value.selectedDate.date == selectedDate.date) { + updateWeekVisibility(!_uiState.value.isWeekEnabled) } else { _uiState.update { currentState -> currentState.copy( @@ -33,27 +27,23 @@ class CalendarViewModel @Inject constructor() : ViewModel() { } } - fun updateSelectedDate(date: LocalDate) = viewModelScope.launch { - _uiState.update { currentState -> + fun updateSelectedDate(value: DayModel) = _uiState.update { currentState -> currentState.copy( - selectedDate = date + selectedDate = value ) } - } - fun updateListVisibility(visibility: Boolean) = viewModelScope.launch { - _uiState.update { currentState -> + + fun updateListVisibility(value: Boolean) = _uiState.update { currentState -> currentState.copy( - isListEnabled = visibility + isListEnabled = value ) } - } - fun updateWeekVisibility(visibility: Boolean) = viewModelScope.launch { - _uiState.update { currentState -> + + fun updateWeekVisibility(value: Boolean) = _uiState.update { currentState -> currentState.copy( - isWeekEnabled = visibility + isWeekEnabled = value ) } - } } \ No newline at end of file diff --git a/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/component/ScreenTransition.kt b/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/component/ScreenTransition.kt index 21d8acdd7..aeebc9557 100644 --- a/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/component/ScreenTransition.kt +++ b/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/component/ScreenTransition.kt @@ -3,20 +3,28 @@ package com.terning.feature.calendar.calendar.component import androidx.compose.animation.AnimatedContent import androidx.compose.animation.ContentTransform import androidx.compose.animation.SizeTransform +import androidx.compose.animation.core.Transition import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import com.terning.feature.calendar.R + +/** + * 두 화면 간 전환을 담당하는 컴포넌트 + * + * @param transition [Transition] + * @param transitionOne 첫번째 화면에서 두번쨰 화면으로 이동할 때 발생할 전환 모션 + * @param transitionTwo 두번째 화면에서 첫번째 화면으로 이동할 때 발생할 전환 모션 + * @param contentOne 첫번째 화면 + * @param contentTwo 두번째 화면 + */ @Composable fun ScreenTransition( - targetState: Boolean, + transition: Transition, transitionOne: ContentTransform, transitionTwo: ContentTransform, contentOne: @Composable () -> Unit, contentTwo: @Composable () -> Unit ) { - AnimatedContent( - targetState = targetState, + transition.AnimatedContent( transitionSpec = { if (targetState) { transitionOne @@ -26,7 +34,6 @@ fun ScreenTransition( sizeTransform = SizeTransform(clip = true) ) }, - label = stringResource(id = R.string.calendar_animation_label) ) { state -> if (state) { contentOne.invoke() diff --git a/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/component/WeekDaysHeader.kt b/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/component/WeekDaysHeader.kt index be398b0c4..0e7d08fa1 100644 --- a/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/component/WeekDaysHeader.kt +++ b/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/component/WeekDaysHeader.kt @@ -8,7 +8,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -16,7 +15,7 @@ import com.terning.core.designsystem.theme.Black import com.terning.core.designsystem.theme.SundayRed import com.terning.core.designsystem.theme.TerningPointTheme import com.terning.core.designsystem.theme.TerningTheme -import com.terning.feature.calendar.R +import com.terning.feature.calendar.calendar.type.WeekDay @Composable fun WeekDaysHeader( @@ -31,21 +30,12 @@ fun WeekDaysHeader( vertical = 18.dp ), ) { - val dayOfWeek = listOf( - R.string.calendar_text_sunday, - R.string.calendar_text_monday, - R.string.calendar_text_tuesday, - R.string.calendar_text_wednesday, - R.string.calendar_text_thursday, - R.string.calendar_text_friday, - R.string.calendar_text_saturday, - ) - dayOfWeek.forEach { day -> + WeekDay.entries.forEach { day -> Text( modifier = Modifier.weight(1f), - text = stringResource(id = day), + text = day.nameInKorean, style = TerningTheme.typography.body7, - color = if (day == R.string.calendar_text_sunday) SundayRed else Black, + color = if (WeekDay.isSunday(day)) SundayRed else Black, textAlign = TextAlign.Center ) } diff --git a/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/component/dialog/CalendarScrapCancelDialog.kt b/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/component/dialog/CalendarScrapCancelDialog.kt new file mode 100644 index 000000000..60c62d543 --- /dev/null +++ b/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/component/dialog/CalendarScrapCancelDialog.kt @@ -0,0 +1,28 @@ +package com.terning.feature.calendar.calendar.component.dialog + +import androidx.compose.runtime.Composable +import com.terning.feature.dialog.cancel.ScrapCancelDialog + +/** + * 달력 스크랩 취소 다이얼로그 + * + * @param scrapVisibility 스크랩 취소 다이얼로그 가시 여부 + * @param internshipAnnouncementId 스크랩 취소를 진행할 공고 ID + * @param onDismissCancelDialog 스크랩 취소 다이얼로그 끄기 + */ + +@Composable +internal fun CalendarScrapCancelDialog( + scrapVisibility: Boolean, + internshipAnnouncementId: Long?, + onDismissCancelDialog: (Boolean) -> Unit, +) { + if (scrapVisibility) { + internshipAnnouncementId?.run { + ScrapCancelDialog( + internshipAnnouncementId = this, + onDismissRequest = onDismissCancelDialog + ) + } + } +} diff --git a/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/component/dialog/CalendarScrapPatchDialog.kt b/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/component/dialog/CalendarScrapPatchDialog.kt new file mode 100644 index 000000000..9861c3573 --- /dev/null +++ b/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/component/dialog/CalendarScrapPatchDialog.kt @@ -0,0 +1,50 @@ +package com.terning.feature.calendar.calendar.component.dialog + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import com.terning.core.designsystem.extension.getFullDateStringInKorean +import com.terning.domain.calendar.entity.CalendarScrapDetail +import com.terning.feature.dialog.detail.ScrapDialog +import java.time.LocalDate + +/** + * 달력 스크랩 디테일 다이얼로그 + * + * @param date 선택된 날짜 + * @param dialogVisibility 스크랩 디테일 다이얼로그 가시 여부 + * @param internshipModel 스크랩 디테일 + * @param navigateToAnnouncement 공고 상세로 이동하는 이동 + * @param onDismissInternDialog 스크랩 디테일 다이얼로그 끄기 + * @param onClickChangeColor 스크랩 색상 변경 시 발생하는 이벤트 + */ + +@Composable +internal fun CalendarScrapPatchDialog( + date: LocalDate, + dialogVisibility: Boolean, + internshipModel: CalendarScrapDetail?, + navigateToAnnouncement: (Long) -> Unit, + onDismissInternDialog: (Boolean) -> Unit, + onClickChangeColor: () -> Unit, +) { + if (dialogVisibility && internshipModel != null) { + val scrapColor = Color( + android.graphics.Color.parseColor( + internshipModel.color + ) + ) + ScrapDialog( + title = internshipModel.title, + scrapColor = scrapColor, + deadline = date.getFullDateStringInKorean(), + startYearMonth = internshipModel.startYearMonth, + workingPeriod = internshipModel.workingPeriod, + internshipAnnouncementId = internshipModel.internshipAnnouncementId, + companyImage = internshipModel.companyImage, + isScrapped = true, + onDismissRequest = onDismissInternDialog, + onClickChangeColor = onClickChangeColor, + onClickNavigateButton = navigateToAnnouncement + ) + } +} diff --git a/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/component/CalendarDay.kt b/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/component/group/CalendarDayGroup.kt similarity index 64% rename from feature/calendar/src/main/java/com/terning/feature/calendar/calendar/component/CalendarDay.kt rename to feature/calendar/src/main/java/com/terning/feature/calendar/calendar/component/group/CalendarDayGroup.kt index 738133119..ffe34f31a 100644 --- a/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/component/CalendarDay.kt +++ b/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/component/group/CalendarDayGroup.kt @@ -1,4 +1,4 @@ -package com.terning.feature.calendar.calendar.component +package com.terning.feature.calendar.calendar.component.group import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -7,12 +7,12 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape 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.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.terning.core.designsystem.extension.noRippleClickable import com.terning.core.designsystem.theme.Black import com.terning.core.designsystem.theme.Grey150 import com.terning.core.designsystem.theme.Grey200 @@ -20,30 +20,36 @@ import com.terning.core.designsystem.theme.TerningMain import com.terning.core.designsystem.theme.TerningPointTheme import com.terning.core.designsystem.theme.TerningTheme import com.terning.core.designsystem.theme.White -import com.terning.feature.calendar.month.model.DayModel +import com.terning.feature.calendar.calendar.model.DayModel import java.time.LocalDate +/** + * 날짜를 표시하는 컴포넌트 그룹 + * + * @param dayModel 날짜에 대한 정보 + * @param isSelected 해당 날짜가 선택된 날짜인지에 대한 여부 + * @param isToday 해당 날짜가 오늘 날짜인지에 대한 여부 + * @param modifier 수정자 + */ + @Composable -fun CalendarDay( - modifier: Modifier = Modifier, - dayData: DayModel, +fun CalendarDayGroup( + dayModel: DayModel, isSelected: Boolean, isToday: Boolean, - onDateSelected: (LocalDate) -> Unit = {} + modifier: Modifier = Modifier, ) { - val backgroundColor = - if (isSelected) - TerningMain + val backgroundColor = remember(isSelected) { + if (isSelected) TerningMain else if (isToday) Grey150 else Color.Transparent + } - val textColor = - if (dayData.isOutDate) - Grey200 - else if (isSelected) - White - else - Black + val textColor = remember(isSelected) { + if (dayModel.isOutDate) Grey200 + else if (isSelected) White + else Black + } Box( @@ -53,11 +59,6 @@ fun CalendarDay( Box( modifier = Modifier .size(22.dp) - .noRippleClickable { - if (!dayData.isOutDate) { - onDateSelected(dayData.date) - } - } .background( color = backgroundColor, shape = CircleShape @@ -65,7 +66,7 @@ fun CalendarDay( contentAlignment = Alignment.Center ) { Text( - text = dayData.date.dayOfMonth.toString(), + text = dayModel.date.dayOfMonth.toString(), color = textColor, style = TerningTheme.typography.calendar ) @@ -78,23 +79,20 @@ fun CalendarDay( fun CalendarDayPreview() { TerningPointTheme { Row { - CalendarDay( - dayData = DayModel(LocalDate.now(), false), + CalendarDayGroup( + dayModel = DayModel(LocalDate.now(), 0, false), isSelected = true, isToday = true, - onDateSelected = {} ) - CalendarDay( - dayData = DayModel(LocalDate.now(), false), + CalendarDayGroup( + dayModel = DayModel(LocalDate.now(), 0, false), isSelected = false, isToday = true, - onDateSelected = {} ) - CalendarDay( - dayData = DayModel(LocalDate.now(), false), + CalendarDayGroup( + dayModel = DayModel(LocalDate.now(), 0, false), isSelected = false, isToday = false, - onDateSelected = {} ) } } diff --git a/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/component/group/CalendarMonthGroup.kt b/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/component/group/CalendarMonthGroup.kt new file mode 100644 index 000000000..3115c6911 --- /dev/null +++ b/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/component/group/CalendarMonthGroup.kt @@ -0,0 +1,109 @@ +package com.terning.feature.calendar.calendar.component.group + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.HorizontalDivider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.terning.core.designsystem.theme.Grey150 +import com.terning.core.designsystem.theme.TerningPointTheme +import com.terning.core.designsystem.theme.White +import com.terning.domain.calendar.entity.CalendarScrap +import com.terning.feature.calendar.calendar.model.DayModel +import com.terning.feature.calendar.calendar.model.MonthModel +import java.time.LocalDate +import java.time.YearMonth + +/** + * 한 달을 표시하는 컴포넌트 + * + * @param dayModels 한달에 포함된 날들에 대한 데이터 리스트 + * @param isWeekEnabled 주간 달력일 때만 선택된 날에 초록색 마크를 달기 위해 사용하는 부울 변수. + * @param onDateSelected 날이 선택돼었을 때 발생하는 이벤트 + * @param selectedDate 선택된 날 + * @param modifier 수정자 + * @param scrapMap 스크랩 목록과 날짜가 매핑된 목록 + * + */ + +@Composable +internal fun CalendarMonthGroup( + dayModels: List>, + isWeekEnabled: Boolean, + onDateSelected: (DayModel) -> Unit, + selectedDate: DayModel, + modifier: Modifier = Modifier, + scrapMap: Map> = emptyMap() +) { + val scrapCount = remember(dayModels) { + if (dayModels.size == 5) SCRAP_COUNT_WEEK_FIVE + else SCRAP_COUNT_WEEK_SIX + } + + Column( + modifier = modifier + .fillMaxSize() + .background(White) + .padding(horizontal = 20.dp) + ) { + dayModels.forEach {week -> + CalendarWeekGroup( + dayModels = week, + isWeekEnabled = isWeekEnabled, + onDateSelected = onDateSelected, + selectedDay = selectedDate, + scrapCount = scrapCount, + scrapMap = scrapMap, + modifier = Modifier + .weight(1f) + .padding(top = 16.dp) + ) + + if (dayModels.indexOf(week) != dayModels.lastIndex) { + HorizontalDivider( + thickness = 1.dp, + color = Grey150, + ) + } + } + } +} + + +private const val SCRAP_COUNT_WEEK_SIX = 3 +private const val SCRAP_COUNT_WEEK_FIVE = 4 + +@Preview(showSystemUi = true) +@Composable +private fun CalendarMonthGroupPreview() { + TerningPointTheme { + val monthModel = MonthModel(YearMonth.now()) + + CalendarMonthGroup( + dayModels = monthModel.calendarMonth, + onDateSelected = {}, + selectedDate = DayModel( + date = LocalDate.now(), + weekIndex = 1, + isOutDate = false + ), + scrapMap = mapOf( + "2024-12-11" to listOf( + CalendarScrap( + scrapId = 1, + title = "테스트1", + deadLine = "2024-12-11", + color = "#FF0F0F", + isScrapped = true + ) + ) + ), + isWeekEnabled = false, + ) + } +} \ No newline at end of file diff --git a/feature/calendar/src/main/java/com/terning/feature/calendar/month/component/CalendarMonthScrap.kt b/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/component/group/CalendarScrapGroup.kt similarity index 61% rename from feature/calendar/src/main/java/com/terning/feature/calendar/month/component/CalendarMonthScrap.kt rename to feature/calendar/src/main/java/com/terning/feature/calendar/calendar/component/group/CalendarScrapGroup.kt index c8771a876..201689b64 100644 --- a/feature/calendar/src/main/java/com/terning/feature/calendar/month/component/CalendarMonthScrap.kt +++ b/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/component/group/CalendarScrapGroup.kt @@ -1,10 +1,9 @@ -package com.terning.feature.calendar.month.component +package com.terning.feature.calendar.calendar.component.group import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -19,21 +18,23 @@ import com.terning.core.designsystem.theme.White import com.terning.domain.calendar.entity.CalendarScrap import com.terning.feature.calendar.R -private const val SCRAP_COUNT_WEEK_SIX = 3 -private const val SCRAP_COUNT_WEEK_FIVE = 4 +/** + * 달력 위에 띠지를 표시하는 컴포넌트 + * + * @param scrapCount 한칸에 표시 가능한 최대 스크랩 + * @param scrapLists 스크랩 목록 + */ @Composable -internal fun CalendarMonthScrap( - weekCount: Int, +internal fun CalendarScrapGroup( + scrapCount: Int, + scrapLists: List, modifier: Modifier = Modifier, - scrapLists: List ) { - val scrapCount = if(weekCount == 5) SCRAP_COUNT_WEEK_FIVE else SCRAP_COUNT_WEEK_SIX - - LazyColumn( + Column( modifier = modifier ) { - items(scrapLists.subList(0, scrapCount.coerceAtMost(scrapLists.size))) { scrap -> + scrapLists.subList(0, scrapCount.coerceAtMost(scrapLists.size)).forEach { scrap -> Text( text = scrap.title, style = TerningTheme.typography.button5, @@ -51,17 +52,18 @@ internal fun CalendarMonthScrap( ) ) } - - item { - if (scrapLists.size > scrapCount) { - Text( - text = stringResource(id = R.string.calendar_scrap_overflow, (scrapLists.size - scrapCount)), - style = TerningTheme.typography.detail4, - textAlign = TextAlign.End, - modifier = Modifier.fillMaxWidth() - ) - } + if (scrapLists.size > scrapCount) { + Text( + text = stringResource( + id = R.string.calendar_scrap_overflow, + (scrapLists.size - scrapCount) + ), + style = TerningTheme.typography.detail4, + textAlign = TextAlign.End, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 2.dp) + ) } } -} - +} \ No newline at end of file diff --git a/feature/calendar/src/main/java/com/terning/feature/calendar/list/component/CalendarScrapList.kt b/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/component/group/CalendarScrapListGroup.kt similarity index 90% rename from feature/calendar/src/main/java/com/terning/feature/calendar/list/component/CalendarScrapList.kt rename to feature/calendar/src/main/java/com/terning/feature/calendar/calendar/component/group/CalendarScrapListGroup.kt index bb59e2658..61695b27d 100644 --- a/feature/calendar/src/main/java/com/terning/feature/calendar/list/component/CalendarScrapList.kt +++ b/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/component/group/CalendarScrapListGroup.kt @@ -1,4 +1,4 @@ -package com.terning.feature.calendar.list.component +package com.terning.feature.calendar.calendar.component.group import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -12,7 +12,7 @@ import androidx.compose.ui.unit.dp import com.terning.domain.calendar.entity.CalendarScrapDetail @Composable -internal fun CalendarScrapList( +internal fun CalendarScrapListGroup( scrapList: List, onScrapButtonClicked: (Long) -> Unit, onInternshipClicked: (CalendarScrapDetail) -> Unit, @@ -32,7 +32,7 @@ internal fun CalendarScrapList( modifier = topModifier ) { for (scrap in scrapList) { - CalendarScrap( + CalendarScrapListItemGroup( scrap = scrap, onScrapButtonClicked = onScrapButtonClicked, onInternshipClicked = onInternshipClicked diff --git a/feature/calendar/src/main/java/com/terning/feature/calendar/list/component/CalendarScrap.kt b/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/component/group/CalendarScrapListItemGroup.kt similarity index 93% rename from feature/calendar/src/main/java/com/terning/feature/calendar/list/component/CalendarScrap.kt rename to feature/calendar/src/main/java/com/terning/feature/calendar/calendar/component/group/CalendarScrapListItemGroup.kt index 9c2c042ed..df0cce3de 100644 --- a/feature/calendar/src/main/java/com/terning/feature/calendar/list/component/CalendarScrap.kt +++ b/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/component/group/CalendarScrapListItemGroup.kt @@ -1,4 +1,4 @@ -package com.terning.feature.calendar.list.component +package com.terning.feature.calendar.calendar.component.group import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -12,7 +12,7 @@ import com.terning.core.designsystem.theme.Grey200 import com.terning.domain.calendar.entity.CalendarScrapDetail @Composable -fun CalendarScrap( +fun CalendarScrapListItemGroup( scrap: CalendarScrapDetail, onScrapButtonClicked: (Long) -> Unit, onInternshipClicked: (CalendarScrapDetail) -> Unit, diff --git a/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/component/group/CalendarWeekGroup.kt b/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/component/group/CalendarWeekGroup.kt new file mode 100644 index 000000000..c242f3a87 --- /dev/null +++ b/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/component/group/CalendarWeekGroup.kt @@ -0,0 +1,157 @@ +package com.terning.feature.calendar.calendar.component.group + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.terning.core.designsystem.extension.noRippleClickable +import com.terning.core.designsystem.theme.TerningPointTheme +import com.terning.core.designsystem.theme.White +import com.terning.domain.calendar.entity.CalendarScrap +import com.terning.feature.calendar.calendar.model.DayModel +import java.time.LocalDate + +/** + * 일주일을 표시하기 위한 컴포넌트 + * + * @param selectedDay 현재 선택된 날짜 + * @param dayModels 해당 주에 포함된 날짜들에 대한 DayModel 목록 + * @param onDateSelected 날짜를 선택했을 때 호출되는 콜백 + * @param scrapCount 표시 가능한 스크랩 개수 + * @param scrapMap 날짜-스크랩 쌍을 담은 맵 + */ + +@Composable +internal fun CalendarWeekGroup( + dayModels: List, + isWeekEnabled: Boolean, + onDateSelected: (DayModel) -> Unit, + scrapCount: Int, + selectedDay: DayModel, + modifier: Modifier = Modifier, + scrapMap: Map> = emptyMap() +) { + Row( + modifier = modifier + ){ + dayModels.forEach { dayModel -> + Column( + modifier = Modifier + .weight(1f) + .noRippleClickable { + if (!dayModel.isOutDate) { + onDateSelected(dayModel) + } + }, + horizontalAlignment = Alignment.CenterHorizontally + ) { + CalendarDayGroup( + dayModel = dayModel, + isSelected = dayModel == selectedDay && isWeekEnabled, + isToday = dayModel.isToday(), + ) + + if (!dayModel.isOutDate && !isWeekEnabled) { + val scrapMapKey = dayModel.getScrapMapKey() + CalendarScrapGroup( + scrapCount = scrapCount, + scrapLists = scrapMap[scrapMapKey].orEmpty(), + modifier = Modifier.fillMaxWidth() + ) + } + } + } + } +} + +@Preview +@Composable +private fun CalendarWeekGroupNoScrapPreview() { + TerningPointTheme { + var selectedDay by remember { mutableStateOf(DayModel(date = LocalDate.now(), weekIndex = 0, isOutDate = false)) } + + CalendarWeekGroup( + selectedDay = selectedDay, + isWeekEnabled = false, + dayModels = listOf( + DayModel(date = LocalDate.now().minusDays(3), weekIndex = 0, isOutDate = false), + DayModel(date = LocalDate.now().minusDays(2), weekIndex = 0, isOutDate = false), + DayModel(date = LocalDate.now().minusDays(1), weekIndex = 0, isOutDate = false), + DayModel(date = LocalDate.now(), weekIndex = 0, isOutDate = false), + DayModel(date = LocalDate.now().plusDays(1), weekIndex = 0, isOutDate = false), + DayModel(date = LocalDate.now().plusDays(2), weekIndex = 0, isOutDate = false), + DayModel(date = LocalDate.now().plusDays(3), weekIndex = 0, isOutDate = false) + ), + onDateSelected = { + selectedDay = it + }, + scrapCount = 3, + modifier = Modifier.fillMaxWidth().wrapContentHeight().background(White) + ) + } +} + +@Preview +@Composable +private fun CalendarWeekGroupScrapPreview() { + TerningPointTheme { + + CalendarWeekGroup( + selectedDay = DayModel(date = LocalDate.now().minusDays(1), weekIndex = 0, isOutDate = false), + isWeekEnabled = false, + dayModels = listOf( + DayModel(date = LocalDate.now().minusDays(3), weekIndex = 0, isOutDate = false), + DayModel(date = LocalDate.now().minusDays(2), weekIndex = 0, isOutDate = false), + DayModel(date = LocalDate.now().minusDays(1), weekIndex = 0, isOutDate = false), + DayModel(date = LocalDate.now(), weekIndex = 0, isOutDate = false), + DayModel(date = LocalDate.now().plusDays(1), weekIndex = 0, isOutDate = false), + DayModel(date = LocalDate.now().plusDays(2), weekIndex = 0, isOutDate = false), + DayModel(date = LocalDate.now().plusDays(3), weekIndex = 0, isOutDate = false) + ), + onDateSelected = {}, + scrapCount = 3, + modifier = Modifier.fillMaxWidth().wrapContentHeight().background(White), + scrapMap = mapOf( + "2024-12-10" to listOf( + CalendarScrap( + scrapId = 1, + title = "테스트1", + deadLine = "2024-12-10", + color = "#FF0F0F", + isScrapped = true + ), + CalendarScrap( + scrapId = 1, + title = "테스트2", + deadLine = "2024-12-10", + color = "#00FF00", + isScrapped = true + ), + CalendarScrap( + scrapId = 1, + title = "테스트3", + deadLine = "2024-12-10", + color = "#0000FF", + isScrapped = true + ), + CalendarScrap( + scrapId = 1, + title = "테스트4", + deadLine = "2024-12-10", + color = "#FF00FF", + isScrapped = true + ) + ) + ) + ) + } +} \ No newline at end of file diff --git a/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/component/pager/CalendarMonthPager.kt b/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/component/pager/CalendarMonthPager.kt new file mode 100644 index 000000000..21a8debd2 --- /dev/null +++ b/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/component/pager/CalendarMonthPager.kt @@ -0,0 +1,82 @@ +package com.terning.feature.calendar.calendar.component.pager + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.terning.core.designsystem.theme.TerningPointTheme +import com.terning.domain.calendar.entity.CalendarScrap +import com.terning.feature.calendar.calendar.component.group.CalendarMonthGroup +import com.terning.feature.calendar.calendar.model.DayModel +import com.terning.feature.calendar.calendar.model.TerningCalendarModel +import java.time.LocalDate + + +@Composable +internal fun CalendarMonthPager( + pagerState: PagerState, + calendarModel: TerningCalendarModel, + onDateSelect: (DayModel) -> Unit, + selectedDate: DayModel, + modifier: Modifier = Modifier, + scrapMap: Map>, + isWeekEnabled: Boolean = false, +) { + HorizontalPager( + state = pagerState, + modifier = modifier.fillMaxSize() + ) { page -> + val monthModel = calendarModel.getMonthModelByPage(page = page) + + CalendarMonthGroup( + isWeekEnabled = isWeekEnabled, + dayModels = monthModel.calendarMonth, + modifier = Modifier.fillMaxSize(), + onDateSelected = onDateSelect, + selectedDate = selectedDate, + scrapMap = scrapMap + ) + } +} + +@Preview +@Composable +private fun CalendarMonthScreenPreview() { + TerningPointTheme { + val calendarModel = TerningCalendarModel() + val selectedDay = DayModel(LocalDate.now()) + val pagerState = rememberPagerState( + initialPage = calendarModel.initialPage, + pageCount = { calendarModel.pageCount } + ) + + HorizontalPager( + state = pagerState, + modifier = Modifier.fillMaxSize() + ) { page -> + val monthModel = calendarModel.getMonthModelByPage(page = page) + + CalendarMonthGroup( + isWeekEnabled = false, + dayModels = monthModel.calendarMonth, + modifier = Modifier.fillMaxSize(), + onDateSelected = { }, + selectedDate = selectedDay, + scrapMap = mapOf( + "2024-12-11" to listOf( + CalendarScrap( + scrapId = 1, + title = "테스트1", + deadLine = "2024-12-11", + isScrapped = true, + color = "#a3d711" + ) + ) + ) + ) + } + } +} \ No newline at end of file diff --git a/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/component/pager/CalendarWeekPager.kt b/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/component/pager/CalendarWeekPager.kt new file mode 100644 index 000000000..a43000d81 --- /dev/null +++ b/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/component/pager/CalendarWeekPager.kt @@ -0,0 +1,74 @@ +package com.terning.feature.calendar.calendar.component.pager + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.terning.core.designsystem.theme.TerningPointTheme +import com.terning.core.designsystem.theme.White +import com.terning.feature.calendar.calendar.component.group.CalendarWeekGroup +import com.terning.feature.calendar.calendar.model.DayModel +import com.terning.feature.calendar.calendar.model.MonthModel +import com.terning.feature.calendar.calendar.model.TerningCalendarModel +import java.time.LocalDate + +@Composable +internal fun CalendarWeekPager( + monthModel: MonthModel, + onDateSelect: (DayModel) -> Unit, + selectedDate: DayModel, + modifier: Modifier = Modifier, +) { + val pagerState = rememberPagerState( + initialPage = selectedDate.weekIndex, + pageCount = { monthModel.calendarMonth.size } + ) + + LaunchedEffect(selectedDate) { + val page = selectedDate.weekIndex + pagerState.animateScrollToPage(page) + } + + HorizontalPager( + modifier = modifier + .fillMaxWidth() + .background(color = White) + .padding(vertical = 16.dp), + contentPadding = PaddingValues(horizontal = 20.dp), + pageSpacing = 32.dp, + state = pagerState, + ) { page -> + + CalendarWeekGroup( + isWeekEnabled = true, + dayModels = monthModel.calendarMonth[page], + onDateSelected = { dayModel -> + onDateSelect(dayModel) + }, + selectedDay = selectedDate, + scrapCount = 0, + ) + } + +} + + +@Preview +@Composable +private fun CalendarWeekPagerPreview() { + TerningPointTheme { + CalendarWeekPager( + monthModel = TerningCalendarModel().getMonthModelByPage(LocalDate.now()), + onDateSelect = { }, + selectedDate = DayModel(LocalDate.now()) + ) + } +} + diff --git a/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/model/CalendarModel.kt b/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/model/CalendarModel.kt deleted file mode 100644 index 9d5c69e90..000000000 --- a/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/model/CalendarModel.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.terning.feature.calendar.calendar.model - -import androidx.compose.runtime.Immutable -import java.time.LocalDate -import java.time.YearMonth - -@Immutable -class CalendarModel internal constructor( - startYear: Int = START_YEAR, - endYear: Int = END_YEAR -) { - private val currentDate = LocalDate.now() - private val startYearDate = LocalDate.of(startYear, 1, 1) - private val endYearDate = LocalDate.of(endYear, 12, 31) - - val pageCount = (endYearDate.year - startYearDate.year) * 12 - val initialPage = (currentDate.year - startYearDate.year) * 12 + currentDate.monthValue - 1 - - companion object { - const val START_YEAR = 2020 - const val END_YEAR = 2030 - - fun getLocalDateByPage(page: Int): LocalDate = LocalDate.of( - START_YEAR + page / 12, - page % 12 + 1, - 1 - ) - - fun getYearMonthByPage(page: Int): YearMonth = YearMonth.of( - START_YEAR + page / 12, - page % 12 + 1, - ) - } -} \ No newline at end of file diff --git a/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/model/CalendarUiState.kt b/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/model/CalendarUiState.kt index 6231be95b..1373729b0 100644 --- a/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/model/CalendarUiState.kt +++ b/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/model/CalendarUiState.kt @@ -1,16 +1,8 @@ package com.terning.feature.calendar.calendar.model -import androidx.compose.foundation.pager.PagerState -import androidx.compose.runtime.compositionLocalOf -import java.time.LocalDate - data class CalendarUiState( - val selectedDate: LocalDate = LocalDate.now(), - val calendarModel: CalendarModel = CalendarModel(), + val selectedDate: DayModel = DayModel(), + val calendarModel: TerningCalendarModel = TerningCalendarModel(), val isListEnabled: Boolean = false, - val isWeekEnabled: Boolean = false -) - -val LocalPagerState = compositionLocalOf { - error("No PagerState provided") -} + val isWeekEnabled: Boolean = false, +) \ No newline at end of file diff --git a/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/model/DayModel.kt b/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/model/DayModel.kt new file mode 100644 index 000000000..858d6efc2 --- /dev/null +++ b/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/model/DayModel.kt @@ -0,0 +1,27 @@ +package com.terning.feature.calendar.calendar.model + +import androidx.compose.runtime.Immutable +import java.time.LocalDate +import java.time.format.DateTimeFormatter + +/** + * 달력의 하루를 나타내는 데이터클래스 모델 + * + * @param date 날짜 + * @param weekIndex 속한 주의 인덱스 + * @param isOutDate 이번 월에 포함되지 않는 날인지에 대한 불리언 값 + */ + +@Immutable +data class DayModel( + val date: LocalDate = LocalDate.now(), + val weekIndex: Int = 0, + val isOutDate: Boolean = false +) { + fun isToday(): Boolean = date == LocalDate.now() + + fun getScrapMapKey(): String { + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") + return date.format(formatter) + } +} \ No newline at end of file diff --git a/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/model/MonthModel.kt b/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/model/MonthModel.kt new file mode 100644 index 000000000..41f5a4271 --- /dev/null +++ b/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/model/MonthModel.kt @@ -0,0 +1,54 @@ +package com.terning.feature.calendar.calendar.model + +import androidx.compose.runtime.Immutable +import java.time.YearMonth + +/** + * 달력에서 한 달에 대한 정보를 관리하는 데이터 클래스 + * + * @property [inDays] 현재 달에 표시돼야 할 이전 달의 날 수 + * @property [monthDays] 현재 달의 날 수 + * @property [outDays] 현재 달에 표시돼야 할 다음 달의 날 수 + * @property [totalDays] 달에 포함된 할 전체 날 수 + * @property [calendarMonth] [DayModel]로 이뤄진 달의 날들의 목록 + */ + +@Immutable +data class MonthModel( + val yearMonth: YearMonth +) { + private val firstDayOfWeek = yearMonth.atDay(1).dayOfWeek.value + + val inDays = firstDayOfWeek % 7 + val monthDays = yearMonth.lengthOfMonth() + val outDays = (7 - ((inDays + monthDays) % 7)) % 7 + val totalDays = monthDays + inDays + outDays + + private val rows = (0 until totalDays).chunked(7) + + val calendarMonth: List> = + rows.map { week -> week.map { dayOffset -> getDay(dayOffset) } } + + private fun getDay(dayOffset: Int): DayModel { + val firstDayOnCalendar = yearMonth.atDay(1).minusDays(inDays.toLong()) + val date = firstDayOnCalendar.plusDays(dayOffset.toLong()) + val weekIndex = (dayOffset / 7) + val isOutDate = YearMonth.of(date.year, date.monthValue) != yearMonth + + return DayModel(date, weekIndex, isOutDate) + } + + override fun toString(): String { + val yearString = yearMonth.year.toString() + val monthString = yearMonth.monthValue.toString().padStart(2, '0') + + return STRING_FORMAT.format(yearString, monthString) + } + + companion object { + private const val STRING_FORMAT = "%s년 %s월" + } + +} + + diff --git a/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/model/TerningCalendarModel.kt b/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/model/TerningCalendarModel.kt new file mode 100644 index 000000000..6733555f7 --- /dev/null +++ b/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/model/TerningCalendarModel.kt @@ -0,0 +1,73 @@ +package com.terning.feature.calendar.calendar.model + +import androidx.compose.runtime.Immutable +import java.time.LocalDate +import java.time.YearMonth + +/** + * 터닝의 달력 로직을 관리하는 클래스입니다. + * 달력의 시작과 끝 연도를 설정하여 달력의 크기를 지정합니다. + * + * @param startYear 캘린더의 시작 연도 + * @param endYear 캘린더의 끝 연도 + */ + +@Immutable +class TerningCalendarModel ( + private val startYear: Int = DEFAULT_START_YEAR, + private val endYear: Int = DEFAULT_END_YEAR +) { + private val currentDate = LocalDate.now() + private val startYearDate = LocalDate.of(startYear, 1, 1) + private val endYearDate = LocalDate.of(endYear, 12, 31) + + val pageCount = (endYearDate.year - startYearDate.year) * 12 + val initialPage = (currentDate.year - startYearDate.year) * 12 + currentDate.monthValue - 1 + + /** + * 페이지 값을 사용하여 [MonthModel]을 반환합니다. + */ + fun getMonthModelByPage(page: Int): MonthModel { + val localDate = getLocalDateByPage(page) + return getMonthModelByPage(localDate = localDate) + } + + /** + * [LocalDate]를 사용하여 [MonthModel]을 반환합니다. + */ + fun getMonthModelByPage(localDate: LocalDate): MonthModel { + val yearMonth = YearMonth.of(localDate.year, localDate.monthValue) + return getMonthModelByPage(yearMonth = yearMonth) + } + + /** + * [YearMonth]를 사용하여 [MonthModel]을 반환합니다. + */ + fun getMonthModelByPage(yearMonth: YearMonth): MonthModel { + return MonthModel(yearMonth = yearMonth) + } + + /** + * 현재 페이지의 연도, 월, 1일을 반환합니다. + * @return [LocalDate] + */ + fun getLocalDateByPage(page: Int): LocalDate = LocalDate.of( + startYear + page / 12, + page % 12 + 1, + 1 + ) + + /** + * 현재 페이지의 연도, 월을 반환합니다. + * @return [YearMonth] + */ + fun getYearMonthByPage(page: Int): YearMonth = YearMonth.of( + startYear + page / 12, + page % 12 + 1, + ) + + companion object { + private const val DEFAULT_START_YEAR = 2020 + private const val DEFAULT_END_YEAR = 2030 + } +} \ No newline at end of file diff --git a/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/type/WeekDay.kt b/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/type/WeekDay.kt new file mode 100644 index 000000000..ce5de1cbc --- /dev/null +++ b/feature/calendar/src/main/java/com/terning/feature/calendar/calendar/type/WeekDay.kt @@ -0,0 +1,19 @@ +package com.terning.feature.calendar.calendar.type + +enum class WeekDay( + val nameInKorean: String, +) { + SUNDAY(nameInKorean = "일"), + MONDAY(nameInKorean = "월"), + TUESDAY(nameInKorean = "화"), + WEDNESDAY(nameInKorean = "수"), + THURSDAY(nameInKorean = "목"), + FRIDAY(nameInKorean = "금"), + SATURDAY(nameInKorean = "토"); + + companion object { + fun isSunday(weekDay: WeekDay): Boolean { + return weekDay == SUNDAY + } + } +} diff --git a/feature/calendar/src/main/java/com/terning/feature/calendar/list/CalendarListScreen.kt b/feature/calendar/src/main/java/com/terning/feature/calendar/list/CalendarListScreen.kt index 8178199ef..e8cf68988 100644 --- a/feature/calendar/src/main/java/com/terning/feature/calendar/list/CalendarListScreen.kt +++ b/feature/calendar/src/main/java/com/terning/feature/calendar/list/CalendarListScreen.kt @@ -18,7 +18,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -28,37 +27,38 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.flowWithLifecycle -import com.terning.core.designsystem.state.UiState import com.terning.core.designsystem.extension.getFullDateStringInKorean import com.terning.core.designsystem.extension.isListNotEmpty import com.terning.core.designsystem.extension.toast +import com.terning.core.designsystem.state.UiState import com.terning.core.designsystem.theme.Back import com.terning.core.designsystem.theme.Black import com.terning.core.designsystem.theme.Grey400 import com.terning.core.designsystem.theme.TerningTheme import com.terning.domain.calendar.entity.CalendarScrapDetail import com.terning.feature.calendar.R -import com.terning.feature.calendar.calendar.model.CalendarModel.Companion.getLocalDateByPage -import com.terning.feature.calendar.calendar.model.LocalPagerState -import com.terning.feature.calendar.list.component.CalendarScrapList +import com.terning.feature.calendar.calendar.component.dialog.CalendarScrapCancelDialog +import com.terning.feature.calendar.calendar.component.dialog.CalendarScrapPatchDialog +import com.terning.feature.calendar.calendar.component.group.CalendarScrapListGroup +import com.terning.feature.calendar.calendar.model.TerningCalendarModel import com.terning.feature.calendar.list.model.CalendarListUiState -import com.terning.feature.dialog.cancel.ScrapCancelDialog -import com.terning.feature.dialog.detail.ScrapDialog import okhttp3.internal.toImmutableList import java.time.LocalDate @Composable fun CalendarListRoute( modifier: Modifier = Modifier, + calendarModel: TerningCalendarModel, + pagerState: PagerState, navigateUp: () -> Unit, navigateToAnnouncement: (Long) -> Unit, viewModel: CalendarListViewModel = hiltViewModel(), ) { - val pagerState = LocalPagerState.current val lifecycleOwner = LocalLifecycleOwner.current - val uiState by viewModel.uiState.collectAsStateWithLifecycle(lifecycleOwner) val context = LocalContext.current + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + LaunchedEffect(viewModel.sideEffect, lifecycleOwner) { viewModel.sideEffect.flowWithLifecycle(lifecycle = lifecycleOwner.lifecycle) .collect { sideEffect -> @@ -71,7 +71,7 @@ fun CalendarListRoute( LaunchedEffect(key1 = pagerState) { snapshotFlow { pagerState.settledPage } .collect { settled -> - val date = getLocalDateByPage(settled) + val date = calendarModel.getLocalDateByPage(settled) viewModel.updateCurrentDate(date) viewModel.getScrapMonthList(date) } @@ -84,6 +84,7 @@ fun CalendarListRoute( CalendarListScreen( pagerState = pagerState, uiState = uiState, + calendarModel = calendarModel, modifier = modifier, onClickScrapButton = { scrapId -> with(viewModel) { @@ -99,8 +100,8 @@ fun CalendarListRoute( } ) - CalendarListScrapPatchDialog( - currentDate = uiState.currentDate, + CalendarScrapPatchDialog( + date = uiState.currentDate, dialogVisibility = uiState.scrapDetailDialogVisibility, internshipModel = uiState.internshipModel, navigateToAnnouncement = { announcementId -> @@ -113,7 +114,7 @@ fun CalendarListRoute( }, ) - CalendarListScrapCancelDialog( + CalendarScrapCancelDialog( scrapVisibility = uiState.scrapCancelDialogVisibility, internshipAnnouncementId = uiState.internshipAnnouncementId, onDismissCancelDialog = { isCancelled -> @@ -128,16 +129,17 @@ fun CalendarListRoute( @Composable private fun CalendarListScreen( pagerState: PagerState, + calendarModel: TerningCalendarModel, uiState: CalendarListUiState, onClickInternship: (CalendarScrapDetail) -> Unit, onClickScrapButton: (Long) -> Unit, modifier: Modifier = Modifier, ) { + HorizontalPager( state = pagerState, modifier = modifier ) { page -> - val getDate = getLocalDateByPage(page) LazyColumn( modifier = Modifier @@ -160,31 +162,15 @@ private fun CalendarListScreen( is UiState.Failure -> {} is UiState.Success -> { - val scrapMap = uiState.loadState.data - items(getDate.lengthOfMonth()) { day -> - val currentDate = - LocalDate.of(getDate.year, getDate.monthValue, day + 1) - val dateInKorean = currentDate.getFullDateStringInKorean() - - if (scrapMap[dateInKorean].isListNotEmpty()) { - Text( - text = dateInKorean, - style = TerningTheme.typography.title5, - color = Black, - modifier = Modifier - .padding(start = 24.dp, top = 16.dp, bottom = 15.dp) - ) - - CalendarScrapList( - scrapList = scrapMap[dateInKorean].orEmpty().toImmutableList(), - onScrapButtonClicked = onClickScrapButton, - onInternshipClicked = onClickInternship, - isFromList = true, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp) - ) - } + val date = calendarModel.getLocalDateByPage(page) + + items(date.lengthOfMonth()) { day -> + CalendarListSuccess( + date = LocalDate.of(date.year, date.monthValue, day + 1), + scrapMap = uiState.loadState.data, + onClickScrapButton = onClickScrapButton, + onClickInternship = onClickInternship, + ) } } } @@ -215,50 +201,32 @@ private fun CalendarListEmpty( } @Composable -private fun CalendarListScrapCancelDialog( - scrapVisibility: Boolean, - internshipAnnouncementId: Long?, - onDismissCancelDialog: (Boolean) -> Unit, -) { - if (scrapVisibility) { - internshipAnnouncementId?.run { - ScrapCancelDialog( - internshipAnnouncementId = this, - onDismissRequest = onDismissCancelDialog - ) - } - } -} - -@Composable -private fun CalendarListScrapPatchDialog( - currentDate: LocalDate, - dialogVisibility: Boolean, - internshipModel: CalendarScrapDetail?, - navigateToAnnouncement: (Long) -> Unit, - onDismissInternDialog: (Boolean) -> Unit, - onClickChangeColor: () -> Unit, +private fun CalendarListSuccess( + date: LocalDate, + scrapMap: Map>, + onClickScrapButton: (Long) -> Unit, + onClickInternship: (CalendarScrapDetail) -> Unit, + modifier: Modifier = Modifier, ) { - if (dialogVisibility) { - internshipModel?.let { internship -> - val scrapColor = Color( - android.graphics.Color.parseColor( - internship.color - ) - ) - ScrapDialog( - title = internship.title, - scrapColor = scrapColor, - deadline = currentDate.getFullDateStringInKorean(), - startYearMonth = internship.startYearMonth, - workingPeriod = internship.workingPeriod, - internshipAnnouncementId = internship.internshipAnnouncementId, - companyImage = internship.companyImage, - isScrapped = true, - onDismissRequest = onDismissInternDialog, - onClickChangeColor = onClickChangeColor, - onClickNavigateButton = navigateToAnnouncement - ) - } + val dateInKorean = date.getFullDateStringInKorean() + + if (scrapMap[dateInKorean].isListNotEmpty()) { + Text( + text = dateInKorean, + style = TerningTheme.typography.title5, + color = Black, + modifier = modifier + .padding(start = 24.dp, top = 16.dp, bottom = 15.dp) + ) + + CalendarScrapListGroup( + scrapList = scrapMap[dateInKorean].orEmpty().toImmutableList(), + onScrapButtonClicked = onClickScrapButton, + onInternshipClicked = onClickInternship, + isFromList = true, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp) + ) } } diff --git a/feature/calendar/src/main/java/com/terning/feature/calendar/month/CalendarMonthScreen.kt b/feature/calendar/src/main/java/com/terning/feature/calendar/month/CalendarMonthScreen.kt index 23a57c2b5..b128f0449 100644 --- a/feature/calendar/src/main/java/com/terning/feature/calendar/month/CalendarMonthScreen.kt +++ b/feature/calendar/src/main/java/com/terning/feature/calendar/month/CalendarMonthScreen.kt @@ -1,7 +1,7 @@ package com.terning.feature.calendar.month +import androidx.compose.foundation.background import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.PagerState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -13,27 +13,26 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.flowWithLifecycle -import com.terning.core.designsystem.state.UiState import com.terning.core.designsystem.extension.toast -import com.terning.feature.calendar.calendar.model.CalendarModel.Companion.getLocalDateByPage -import com.terning.feature.calendar.calendar.model.LocalPagerState -import com.terning.feature.calendar.month.component.CalendarMonth -import com.terning.feature.calendar.month.model.CalendarMonthUiState -import com.terning.feature.calendar.month.model.MonthModel -import java.time.LocalDate -import java.time.YearMonth +import com.terning.core.designsystem.state.UiState +import com.terning.core.designsystem.theme.White +import com.terning.domain.calendar.entity.CalendarScrap +import com.terning.feature.calendar.calendar.component.pager.CalendarMonthPager +import com.terning.feature.calendar.calendar.model.DayModel +import com.terning.feature.calendar.calendar.model.TerningCalendarModel @Composable fun CalendarMonthRoute( - selectedDate: LocalDate, - updateSelectedDate: (LocalDate) -> Unit, + selectedDate: DayModel, + calendarModel: TerningCalendarModel, + pagerState: PagerState, + updateSelectedDate: (DayModel) -> Unit, modifier: Modifier = Modifier, viewModel: CalendarMonthViewModel = hiltViewModel() ) { - val pagerState = LocalPagerState.current val context = LocalContext.current val lifecycleOwner = LocalLifecycleOwner.current - val monthUiState by viewModel.uiState.collectAsStateWithLifecycle(lifecycleOwner) + val uiState by viewModel.uiState.collectAsStateWithLifecycle() LaunchedEffect(viewModel.sideEffect, lifecycleOwner) { viewModel.sideEffect.flowWithLifecycle(lifecycle = lifecycleOwner.lifecycle) @@ -46,50 +45,28 @@ fun CalendarMonthRoute( LaunchedEffect(key1 = pagerState) { snapshotFlow { pagerState.currentPage } - .collect { currentPage-> - viewModel.getScrapMonth(currentPage) + .collect { currentPage -> + val localDate = calendarModel.getLocalDateByPage(currentPage) + viewModel.getScrapMonth(localDate) } } - CalendarMonthScreen( + CalendarMonthPager( + modifier = modifier + .fillMaxSize() + .background(White), pagerState = pagerState, + calendarModel = calendarModel, selectedDate = selectedDate, - uiState = monthUiState, - updateSelectedDate = updateSelectedDate, - modifier = modifier + onDateSelect = updateSelectedDate, + isWeekEnabled = false, + scrapMap = when (uiState.loadState) { + UiState.Loading -> emptyMap() + UiState.Empty -> emptyMap() + is UiState.Failure -> emptyMap() + is UiState.Success -> (uiState.loadState as UiState.Success>>).data + }, ) } -@Composable -private fun CalendarMonthScreen( - pagerState: PagerState, - selectedDate: LocalDate, - uiState: CalendarMonthUiState, - updateSelectedDate: (LocalDate) -> Unit, - modifier: Modifier = Modifier, -) { - - HorizontalPager( - state = pagerState, - modifier = modifier.fillMaxSize() - ) {page -> - val date = getLocalDateByPage(page) - val monthModel = MonthModel(YearMonth.of(date.year, date.month)) - - CalendarMonth( - modifier = Modifier.fillMaxSize(), - onDateSelected = updateSelectedDate, - monthModel = monthModel, - selectedDate = selectedDate, - isWeekEnabled = false, - scrapMap = when (uiState.loadState) { - UiState.Loading -> emptyMap() - UiState.Empty -> emptyMap() - is UiState.Failure -> emptyMap() - is UiState.Success -> uiState.loadState.data - }, - ) - } -} - diff --git a/feature/calendar/src/main/java/com/terning/feature/calendar/month/CalendarMonthViewModel.kt b/feature/calendar/src/main/java/com/terning/feature/calendar/month/CalendarMonthViewModel.kt index 0fd23b2ea..a5d1eba03 100644 --- a/feature/calendar/src/main/java/com/terning/feature/calendar/month/CalendarMonthViewModel.kt +++ b/feature/calendar/src/main/java/com/terning/feature/calendar/month/CalendarMonthViewModel.kt @@ -5,7 +5,6 @@ import androidx.lifecycle.viewModelScope import com.terning.core.designsystem.state.UiState import com.terning.domain.calendar.repository.CalendarRepository import com.terning.feature.calendar.R -import com.terning.feature.calendar.calendar.model.CalendarModel.Companion.getLocalDateByPage import com.terning.feature.calendar.month.model.CalendarMonthUiState import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers @@ -15,6 +14,7 @@ import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import java.time.LocalDate import javax.inject.Inject @HiltViewModel @@ -28,9 +28,8 @@ class CalendarMonthViewModel @Inject constructor( val sideEffect = _sideEffect.asSharedFlow() fun getScrapMonth( - currentPage: Int + date: LocalDate ) = viewModelScope.launch(Dispatchers.IO) { - val date = getLocalDateByPage(currentPage) calendarRepository.getScrapMonth(date.year, date.monthValue) .fold( onSuccess = { diff --git a/feature/calendar/src/main/java/com/terning/feature/calendar/month/component/CalendarMonth.kt b/feature/calendar/src/main/java/com/terning/feature/calendar/month/component/CalendarMonth.kt deleted file mode 100644 index d69eb981a..000000000 --- a/feature/calendar/src/main/java/com/terning/feature/calendar/month/component/CalendarMonth.kt +++ /dev/null @@ -1,95 +0,0 @@ -package com.terning.feature.calendar.month.component - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.HorizontalDivider -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.terning.core.designsystem.extension.getDateAsMapString -import com.terning.core.designsystem.extension.isToday -import com.terning.core.designsystem.extension.noRippleClickable -import com.terning.core.designsystem.theme.Grey150 -import com.terning.core.designsystem.theme.TerningPointTheme -import com.terning.domain.calendar.entity.CalendarScrap -import com.terning.feature.calendar.calendar.component.CalendarDay -import com.terning.feature.calendar.month.model.MonthModel -import java.time.LocalDate -import java.time.YearMonth - -@Composable -internal fun CalendarMonth( - isWeekEnabled: Boolean, - monthModel: MonthModel, - onDateSelected: (LocalDate) -> Unit, - selectedDate: LocalDate, - modifier: Modifier = Modifier, - scrapMap: Map> = mapOf() -) { - Column( - modifier = modifier - .fillMaxSize() - .padding(horizontal = 20.dp), - ) { - val month = monthModel.calendarMonth.weekDays - for (week in month) { - Row( - modifier = Modifier.weight(1f), - ) { - for (day in week) { - Column( - modifier = Modifier - .weight(1f) - .fillMaxSize() - .noRippleClickable { - if(!day.isOutDate) { - onDateSelected(day.date) - } - } - .padding(top = 15.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - CalendarDay( - dayData = day, - isSelected = selectedDate == day.date && isWeekEnabled, - isToday = day.date.isToday(), - onDateSelected = onDateSelected - ) - if (!day.isOutDate) { - val index = day.date.getDateAsMapString() - CalendarMonthScrap( - weekCount = month.size, - scrapLists = scrapMap[index].orEmpty(), - modifier = Modifier.fillMaxWidth() - ) - } - } - } - } - if (month.indexOf(week) != month.lastIndex) { - HorizontalDivider( - thickness = 1.dp, - color = Grey150 - ) - } - } - } -} - -@Preview(showBackground = true) -@Composable -private fun CalendarMonthPreview() { - TerningPointTheme { - CalendarMonth( - monthModel = MonthModel(YearMonth.now()), - onDateSelected = {}, - selectedDate = LocalDate.now(), - isWeekEnabled = true - ) - } -} \ No newline at end of file diff --git a/feature/calendar/src/main/java/com/terning/feature/calendar/month/model/DayModel.kt b/feature/calendar/src/main/java/com/terning/feature/calendar/month/model/DayModel.kt deleted file mode 100644 index 9f3563f8c..000000000 --- a/feature/calendar/src/main/java/com/terning/feature/calendar/month/model/DayModel.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.terning.feature.calendar.month.model - -import androidx.compose.runtime.Immutable -import java.time.LocalDate - -@Immutable -data class DayModel( - val date: LocalDate, - val isOutDate: Boolean = false -) \ No newline at end of file diff --git a/feature/calendar/src/main/java/com/terning/feature/calendar/month/model/MonthModel.kt b/feature/calendar/src/main/java/com/terning/feature/calendar/month/model/MonthModel.kt deleted file mode 100644 index f9b940cc9..000000000 --- a/feature/calendar/src/main/java/com/terning/feature/calendar/month/model/MonthModel.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.terning.feature.calendar.month.model - -import androidx.compose.runtime.Immutable -import com.terning.feature.calendar.month.model.MonthModel.MonthModel -import java.time.YearMonth - -/** - * [MonthModel] is responsible for managing a month's overall characteristics - * it consists of following properties:- - * - * [inDays] represents the number of days in the previous month that should be shown before the first day of the month. - * [outDays] represents the number of days in the next month that should be shown after the last day of the month.] - * [totalDays] represents the total number of days shown on calendar - * [calendarMonth] represents the list of days of the month, a list of [DayModel] - */ - - -@Immutable -data class MonthModel( - val month: YearMonth -) { - private val firstDayOfWeek = month.atDay(1).dayOfWeek.value - private val previousMonth = month.minusMonths(1) - private val nextMonth = month.plusMonths(1) - - val inDays = firstDayOfWeek % 7 - val monthDays = month.lengthOfMonth() - val outDays = (7 - ((inDays + monthDays) % 7)) % 7 - val totalDays = monthDays + inDays + outDays - - private val rows = (0 until totalDays).chunked(7) - - val calendarMonth = - MonthModel(month, rows.map { week -> week.map { dayOffset -> getDay(dayOffset) } }) - - private fun getDay(dayOffset: Int): DayModel { - val firstDayOnCalendar = month.atDay(1).minusDays(inDays.toLong()) - val date = firstDayOnCalendar.plusDays(dayOffset.toLong()) - val isOutDate = YearMonth.of(date.year, date.monthValue) != month - - return DayModel(date, isOutDate) - } - - @Immutable - data class MonthModel( - val yearMonth: YearMonth, - val weekDays: List> - ) { - override fun toString(): String { - return "${yearMonth.year}년 ${yearMonth.monthValue.toString().padStart(2, '0')}월" - } - } -} - - diff --git a/feature/calendar/src/main/java/com/terning/feature/calendar/week/CalendarWeekScreen.kt b/feature/calendar/src/main/java/com/terning/feature/calendar/week/CalendarWeekScreen.kt index cb7141efa..ab22b7fe5 100644 --- a/feature/calendar/src/main/java/com/terning/feature/calendar/week/CalendarWeekScreen.kt +++ b/feature/calendar/src/main/java/com/terning/feature/calendar/week/CalendarWeekScreen.kt @@ -4,13 +4,16 @@ import androidx.activity.compose.BackHandler import androidx.compose.foundation.Image 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.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -21,7 +24,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.shadow -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -31,38 +33,38 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.flowWithLifecycle -import com.terning.core.designsystem.state.UiState import com.terning.core.designsystem.extension.getDateStringInKorean -import com.terning.core.designsystem.extension.getFullDateStringInKorean import com.terning.core.designsystem.extension.swipableVertically import com.terning.core.designsystem.extension.toast +import com.terning.core.designsystem.state.UiState import com.terning.core.designsystem.theme.Back import com.terning.core.designsystem.theme.Black +import com.terning.core.designsystem.theme.Grey200 import com.terning.core.designsystem.theme.Grey400 import com.terning.core.designsystem.theme.TerningTheme import com.terning.core.designsystem.theme.White import com.terning.domain.calendar.entity.CalendarScrapDetail import com.terning.feature.calendar.R -import com.terning.feature.calendar.calendar.model.CalendarUiState -import com.terning.feature.calendar.calendar.model.LocalPagerState -import com.terning.feature.calendar.list.component.CalendarScrapList -import com.terning.feature.calendar.week.component.HorizontalCalendarWeek +import com.terning.feature.calendar.calendar.component.dialog.CalendarScrapCancelDialog +import com.terning.feature.calendar.calendar.component.dialog.CalendarScrapPatchDialog +import com.terning.feature.calendar.calendar.component.group.CalendarScrapListGroup +import com.terning.feature.calendar.calendar.component.pager.CalendarWeekPager +import com.terning.feature.calendar.calendar.model.DayModel +import com.terning.feature.calendar.calendar.model.TerningCalendarModel import com.terning.feature.calendar.week.model.CalendarWeekUiState -import com.terning.feature.dialog.cancel.ScrapCancelDialog -import com.terning.feature.dialog.detail.ScrapDialog import okhttp3.internal.toImmutableList -import java.time.LocalDate @Composable fun CalendarWeekRoute( - calendarUiState: CalendarUiState, + selectedDate: DayModel, + calendarModel: TerningCalendarModel, + pagerState: PagerState, navigateToAnnouncement: (Long) -> Unit, - updateSelectedDate: (LocalDate) -> Unit, + updateSelectedDate: (DayModel) -> Unit, navigateUp: () -> Unit, modifier: Modifier = Modifier, viewModel: CalendarWeekViewModel = hiltViewModel(), ) { - val pagerState = LocalPagerState.current val lifecycleOwner = LocalLifecycleOwner.current val uiState by viewModel.uiState.collectAsStateWithLifecycle(lifecycleOwner) @@ -76,8 +78,8 @@ fun CalendarWeekRoute( } } - LaunchedEffect(key1 = calendarUiState.selectedDate) { - viewModel.getScrapWeekList(selectedDate = calendarUiState.selectedDate) + LaunchedEffect(key1 = selectedDate) { + viewModel.getScrapWeekList(selectedDate = selectedDate.date) } BackHandler { @@ -85,10 +87,11 @@ fun CalendarWeekRoute( } CalendarWeekScreen( - modifier = modifier, + modifier = modifier.fillMaxSize(), pagerState = pagerState, + calendarModel = calendarModel, uiState = uiState, - selectedDate = calendarUiState.selectedDate, + selectedDate = selectedDate, updateSelectedDate = updateSelectedDate, onClickScrapButton = { scrapId -> with(viewModel) { @@ -104,8 +107,8 @@ fun CalendarWeekRoute( }, ) - CalendarWeekScrapPatchDialog( - currentDate = calendarUiState.selectedDate, + CalendarScrapPatchDialog( + date = selectedDate.date, dialogVisibility = uiState.scrapDetailDialogVisibility, internshipModel = uiState.internshipModel, navigateToAnnouncement = { announcementId -> @@ -114,17 +117,17 @@ fun CalendarWeekRoute( }, onDismissInternDialog = { viewModel.updateScrapDetailDialogVisibility(false) }, onClickChangeColor = { - viewModel.getScrapWeekList(calendarUiState.selectedDate) + viewModel.getScrapWeekList(selectedDate.date) }, ) - CalendarWeekScrapCancelDialog( + CalendarScrapCancelDialog( scrapVisibility = uiState.scrapCancelDialogVisibility, internshipAnnouncementId = uiState.internshipAnnouncementId, onDismissCancelDialog = { isCancelled -> viewModel.updateScrapCancelDialogVisibility(false) if (isCancelled) { - viewModel.getScrapWeekList(calendarUiState.selectedDate) + viewModel.getScrapWeekList(selectedDate.date) } } ) @@ -133,9 +136,10 @@ fun CalendarWeekRoute( @Composable private fun CalendarWeekScreen( uiState: CalendarWeekUiState, + calendarModel: TerningCalendarModel, pagerState: PagerState, - selectedDate: LocalDate, - updateSelectedDate: (LocalDate) -> Unit, + selectedDate: DayModel, + updateSelectedDate: (DayModel) -> Unit, onClickInternship: (CalendarScrapDetail) -> Unit, onClickScrapButton: (Long) -> Unit, modifier: Modifier = Modifier, @@ -160,15 +164,23 @@ private fun CalendarWeekScreen( shape = RoundedCornerShape(bottomStart = 20.dp, bottomEnd = 20.dp), elevation = 1.dp ), - + colors = CardDefaults.cardColors( + containerColor = White + ), shape = RoundedCornerShape(bottomStart = 20.dp, bottomEnd = 20.dp), ) { - HorizontalCalendarWeek( + CalendarWeekPager( + monthModel = calendarModel.getMonthModelByPage(selectedDate.date), selectedDate = selectedDate, - onDateSelected = updateSelectedDate, + onDateSelect = updateSelectedDate, + ) + + Spacer( modifier = Modifier - .fillMaxWidth() - .background(White) + .padding(top = 4.dp, bottom = 8.dp) + .size(width = 55.dp, height = 4.dp) + .background(color = Grey200, shape = RoundedCornerShape(2.dp)) + .align(Alignment.CenterHorizontally), ) } @@ -182,7 +194,7 @@ private fun CalendarWeekScreen( horizontalAlignment = Alignment.CenterHorizontally ) { Text( - text = selectedDate.getDateStringInKorean(), + text = selectedDate.date.getDateStringInKorean(), style = TerningTheme.typography.title5, color = Black, modifier = Modifier @@ -241,7 +253,7 @@ private fun CalendarWeekSuccess( onScrapButtonClicked: (Long) -> Unit, onInternshipClicked: (CalendarScrapDetail) -> Unit, ) { - CalendarScrapList( + CalendarScrapListGroup( scrapList = scrapList, onScrapButtonClicked = onScrapButtonClicked, onInternshipClicked = onInternshipClicked, @@ -250,55 +262,6 @@ private fun CalendarWeekSuccess( ) } -@Composable -private fun CalendarWeekScrapCancelDialog( - scrapVisibility: Boolean, - internshipAnnouncementId: Long?, - onDismissCancelDialog: (Boolean) -> Unit, -) { - if (scrapVisibility) { - internshipAnnouncementId?.run { - ScrapCancelDialog( - internshipAnnouncementId = this, - onDismissRequest = onDismissCancelDialog - ) - } - } -} - -@Composable -private fun CalendarWeekScrapPatchDialog( - currentDate: LocalDate, - dialogVisibility: Boolean, - internshipModel: CalendarScrapDetail?, - navigateToAnnouncement: (Long) -> Unit, - onDismissInternDialog: (Boolean) -> Unit, - onClickChangeColor: () -> Unit, -) { - if (dialogVisibility) { - internshipModel?.let { internship -> - val scrapColor = Color( - android.graphics.Color.parseColor( - internship.color - ) - ) - ScrapDialog( - title = internship.title, - scrapColor = scrapColor, - deadline = currentDate.getFullDateStringInKorean(), - startYearMonth = internship.startYearMonth, - workingPeriod = internship.workingPeriod, - internshipAnnouncementId = internship.internshipAnnouncementId, - companyImage = internship.companyImage, - isScrapped = true, - onDismissRequest = onDismissInternDialog, - onClickChangeColor = onClickChangeColor, - onClickNavigateButton = navigateToAnnouncement - ) - } - } -} - diff --git a/feature/calendar/src/main/java/com/terning/feature/calendar/week/component/HorizontalCalendarWeek.kt b/feature/calendar/src/main/java/com/terning/feature/calendar/week/component/HorizontalCalendarWeek.kt deleted file mode 100644 index bc9b6bf64..000000000 --- a/feature/calendar/src/main/java/com/terning/feature/calendar/week/component/HorizontalCalendarWeek.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.terning.feature.calendar.week.component - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.pager.HorizontalPager -import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import com.terning.core.designsystem.extension.getWeekIndexContainingSelectedDate -import com.terning.core.designsystem.extension.isToday -import com.terning.feature.calendar.calendar.component.CalendarDay -import com.terning.feature.calendar.month.model.MonthModel -import java.time.LocalDate -import java.time.YearMonth - -@Composable -fun HorizontalCalendarWeek( - selectedDate: LocalDate, - onDateSelected: (LocalDate) -> Unit, - modifier: Modifier = Modifier -) { - val monthModel = MonthModel(YearMonth.of(selectedDate.year, selectedDate.monthValue)) - val currentWeek = selectedDate.getWeekIndexContainingSelectedDate(monthModel.inDays) - - val pagerState = rememberPagerState( - initialPage = currentWeek, - pageCount = { monthModel.totalDays / 7 } - ) - - LaunchedEffect(selectedDate) { - pagerState.animateScrollToPage(selectedDate.getWeekIndexContainingSelectedDate(monthModel.inDays)) - } - - HorizontalPager( - modifier = modifier, - state = pagerState - ) { page -> - LazyRow( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 32.dp, vertical = 20.dp), - horizontalArrangement = Arrangement.SpaceBetween - ) { - items(items = monthModel.calendarMonth.weekDays[page]) { day -> - CalendarDay( - dayData = day, - isSelected = day.date == selectedDate, - isToday = day.date.isToday(), - onDateSelected = onDateSelected - ) - } - } - } -} \ No newline at end of file