diff --git a/feature/src/main/java/com/terning/feature/calendar/calendar/CalendarRoute.kt b/feature/src/main/java/com/terning/feature/calendar/calendar/CalendarRoute.kt index 7ed8927a8..37b56da48 100644 --- a/feature/src/main/java/com/terning/feature/calendar/calendar/CalendarRoute.kt +++ b/feature/src/main/java/com/terning/feature/calendar/calendar/CalendarRoute.kt @@ -16,106 +16,89 @@ import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.flowWithLifecycle -import androidx.navigation.NavController -import androidx.navigation.compose.rememberNavController import com.terning.core.designsystem.component.topappbar.CalendarTopAppBar import com.terning.core.designsystem.theme.Grey200 import com.terning.core.designsystem.theme.White -import com.terning.core.extension.toast import com.terning.feature.calendar.calendar.component.ScreenTransition import com.terning.feature.calendar.calendar.component.WeekDaysHeader -import com.terning.feature.calendar.calendar.model.CalendarModel -import com.terning.feature.calendar.list.CalendarListScreen -import com.terning.feature.calendar.month.CalendarMonthScreen -import com.terning.feature.calendar.week.CalendarWeekScreen +import com.terning.feature.calendar.calendar.model.CalendarModel.Companion.getYearMonthByPage +import com.terning.feature.calendar.calendar.model.CalendarUiState +import com.terning.feature.calendar.list.CalendarListRoute +import com.terning.feature.calendar.month.CalendarMonthRoute +import com.terning.feature.calendar.week.CalendarWeekRoute import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch -import java.time.YearMonth +import java.time.LocalDate @Composable fun CalendarRoute( - navController: NavController + navigateUp: () -> Unit, + navigateToAnnouncement: (Long) -> Unit, + viewModel: CalendarViewModel = hiltViewModel() ) { + val lifecycleOwner = LocalLifecycleOwner.current + val uiState by viewModel.uiState.collectAsStateWithLifecycle(lifecycleOwner = lifecycleOwner) + + BackHandler { + if (uiState.isWeekEnabled) { + viewModel.updateSelectedDate(uiState.selectedDate) + } else if (uiState.isListEnabled) { + viewModel.updateListVisibility(false) + } else { + navigateUp() + } + } + CalendarScreen( - navController = navController + uiState = uiState, + navigateToAnnouncement = navigateToAnnouncement, + updateSelectedDate = viewModel::updateSelectedDate, + updatePage = viewModel::updatePage, + onClickListButton = { + viewModel.updateListVisibility(!uiState.isListEnabled) + if (uiState.isWeekEnabled) { viewModel.updateWeekVisibility(false) } + } ) } @Composable private fun CalendarScreen( + uiState: CalendarUiState, + navigateToAnnouncement: (Long) -> Unit, + updateSelectedDate: (LocalDate) -> Unit, + updatePage: (Int) -> Unit, + onClickListButton: () -> Unit, modifier: Modifier = Modifier, - navController: NavController = rememberNavController(), - viewModel: CalendarViewModel = hiltViewModel() ) { - val context = LocalContext.current - val lifecycleOwner = LocalLifecycleOwner.current - val state = CalendarModel() - val calendarUiState by viewModel.uiState.collectAsStateWithLifecycle(lifecycleOwner = lifecycleOwner) + val coroutineScope = rememberCoroutineScope() val listState = rememberLazyListState( - initialFirstVisibleItemIndex = state.initialPage + initialFirstVisibleItemIndex = uiState.calendarModel.initialPage ) - var currentDate by rememberSaveable { mutableStateOf(YearMonth.now()) } - var currentPage by rememberSaveable { mutableIntStateOf(listState.firstVisibleItemIndex) } - - LaunchedEffect(viewModel.calendarSideEffect, lifecycleOwner) { - viewModel.calendarSideEffect.flowWithLifecycle(lifecycle = lifecycleOwner.lifecycle) - .collect { sideEffect -> - when (sideEffect) { - is CalendarSideEffect.ShowToast -> context.toast(sideEffect.message) - } - } - } - LaunchedEffect(key1 = listState) { snapshotFlow { listState.firstVisibleItemIndex } .distinctUntilChanged() .collect { - val swipeDirection = - (listState.firstVisibleItemIndex - currentPage).coerceIn(-1, 1).toLong() - currentDate = currentDate.plusMonths(swipeDirection) - currentPage = listState.firstVisibleItemIndex + updatePage(listState.firstVisibleItemIndex) } } - BackHandler { - if (calendarUiState.isWeekEnabled) { - viewModel.updateSelectedDate(calendarUiState.selectedDate) - } else if (calendarUiState.isListEnabled) { - viewModel.changeListVisibility() - } else { - navController.navigateUp() - } - } - Scaffold( modifier = modifier, topBar = { - val coroutineScope = rememberCoroutineScope() CalendarTopAppBar( - date = currentDate, - isListExpanded = calendarUiState.isListEnabled, - isWeekExpanded = calendarUiState.isWeekEnabled, - onListButtonClicked = { - viewModel.changeListVisibility() - if (calendarUiState.isWeekEnabled) { - viewModel.disableWeekCalendar() - } - }, + date = getYearMonthByPage(uiState.currentPage), + isListExpanded = uiState.isListEnabled, + isWeekExpanded = uiState.isWeekEnabled, + onListButtonClicked = onClickListButton, onMonthNavigationButtonClicked = { direction -> coroutineScope.launch { listState.animateScrollToItem( @@ -127,7 +110,7 @@ private fun CalendarScreen( } ) { paddingValues -> ScreenTransition( - targetState = !calendarUiState.isListEnabled, + targetState = !uiState.isListEnabled, transitionOne = slideInHorizontally { fullWidth -> -fullWidth } togetherWith slideOutHorizontally { fullWidth -> fullWidth }, transitionTwo = slideInHorizontally { fullWidth -> fullWidth } togetherWith @@ -146,39 +129,39 @@ private fun CalendarScreen( ) ScreenTransition( - targetState = !calendarUiState.isWeekEnabled, + targetState = !uiState.isWeekEnabled, transitionOne = slideInVertically { fullHeight -> -fullHeight } togetherWith slideOutVertically { fullHeight -> fullHeight }, transitionTwo = slideInVertically { fullHeight -> fullHeight } togetherWith slideOutVertically { fullHeight -> -fullHeight }, contentOne = { - CalendarMonthScreen( - calendarUiState = calendarUiState, + CalendarMonthRoute( listState = listState, - pages = state.pageCount, + pages = uiState.calendarModel.pageCount, + selectedDate = uiState.selectedDate, + updateSelectedDate = updateSelectedDate, modifier = Modifier .fillMaxSize() .background(White), ) }, contentTwo = { - CalendarWeekScreen( - calendarUiState = calendarUiState, - viewModel = viewModel, - navController = navController, + CalendarWeekRoute( + calendarUiState = uiState, modifier = Modifier .fillMaxSize(), + navigateToAnnouncement = navigateToAnnouncement, + updateSelectedDate = updateSelectedDate ) } ) } }, contentTwo = { - CalendarListScreen( + CalendarListRoute( listState = listState, - pages = state.pageCount, - viewModel = viewModel, - navController = navController, + pages = uiState.calendarModel.pageCount, + navigateToAnnouncement = navigateToAnnouncement, modifier = Modifier .fillMaxSize() .padding(top = paddingValues.calculateTopPadding()) diff --git a/feature/src/main/java/com/terning/feature/calendar/calendar/CalendarSideEffect.kt b/feature/src/main/java/com/terning/feature/calendar/calendar/CalendarSideEffect.kt deleted file mode 100644 index c9026f467..000000000 --- a/feature/src/main/java/com/terning/feature/calendar/calendar/CalendarSideEffect.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.terning.feature.calendar.calendar - -import androidx.annotation.StringRes - -sealed class CalendarSideEffect { - class ShowToast(@StringRes val message: Int) : CalendarSideEffect() -} \ No newline at end of file diff --git a/feature/src/main/java/com/terning/feature/calendar/calendar/CalendarViewModel.kt b/feature/src/main/java/com/terning/feature/calendar/calendar/CalendarViewModel.kt index 3e8a7f8db..4b525140a 100644 --- a/feature/src/main/java/com/terning/feature/calendar/calendar/CalendarViewModel.kt +++ b/feature/src/main/java/com/terning/feature/calendar/calendar/CalendarViewModel.kt @@ -1,253 +1,59 @@ package com.terning.feature.calendar.calendar -import androidx.compose.ui.graphics.Color import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.terning.core.designsystem.theme.CalBlue1 -import com.terning.core.designsystem.theme.CalBlue2 -import com.terning.core.designsystem.theme.CalGreen1 -import com.terning.core.designsystem.theme.CalGreen2 -import com.terning.core.designsystem.theme.CalOrange1 -import com.terning.core.designsystem.theme.CalOrange2 -import com.terning.core.designsystem.theme.CalPink -import com.terning.core.designsystem.theme.CalPurple -import com.terning.core.designsystem.theme.CalRed -import com.terning.core.designsystem.theme.CalYellow -import com.terning.core.state.UiState -import com.terning.domain.entity.CalendarScrapRequest -import com.terning.domain.entity.CalendarScrapDetail -import com.terning.domain.repository.CalendarRepository -import com.terning.domain.repository.ScrapRepository -import com.terning.feature.R import com.terning.feature.calendar.calendar.model.CalendarUiState -import com.terning.feature.calendar.month.model.MonthUiState -import com.terning.feature.calendar.list.model.CalendarListState -import com.terning.feature.calendar.week.model.CalendarWeekState import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import java.time.LocalDate import javax.inject.Inject @HiltViewModel -class CalendarViewModel @Inject constructor( - private val calendarRepository: CalendarRepository, - private val scrapRepository: ScrapRepository -) : ViewModel() { - - private var _uiState: MutableStateFlow = MutableStateFlow( - CalendarUiState( - selectedDate = LocalDate.now(), - isListEnabled = false - ) - ) - +class CalendarViewModel @Inject constructor() : ViewModel() { + private var _uiState: MutableStateFlow = MutableStateFlow(CalendarUiState()) val uiState get() = _uiState.asStateFlow() - private val _MonthUiState = MutableStateFlow(MonthUiState()) - val calendarMonthState = _MonthUiState.asStateFlow() - - private val _calendarListState = MutableStateFlow(CalendarListState()) - val calendarListState = _calendarListState.asStateFlow() - - private val _calendarWeekState = MutableStateFlow(CalendarWeekState()) - val calendarWeekState = _calendarWeekState.asStateFlow() - - private val _calendarSideEffect: MutableSharedFlow = MutableSharedFlow() - val calendarSideEffect = _calendarSideEffect.asSharedFlow() - fun updateSelectedDate(date: LocalDate) = viewModelScope.launch { if (_uiState.value.selectedDate != date) { _uiState.update { currentState -> currentState.copy( - selectedDate = date, - isWeekEnabled = true + selectedDate = date ) } - getScrapWeekList() + updateWeekVisibility(true) } else { - _uiState.update { currentState -> - currentState.copy( - isWeekEnabled = !currentState.isWeekEnabled - ) - } - } - } - - fun changeListVisibility() { - _uiState.update { currentState -> - currentState.copy( - isListEnabled = !currentState.isListEnabled - ) - } - } - - fun disableWeekCalendar() { - _uiState.update { currentState -> - currentState.copy( - isWeekEnabled = false - ) + updateWeekVisibility(!_uiState.value.isWeekEnabled) } } - fun updateScrapCancelDialogVisible(scrapId: Long = -1) { + fun updatePage(page: Int) = viewModelScope.launch { _uiState.update { currentState -> currentState.copy( - isScrapButtonClicked = !currentState.isScrapButtonClicked, - scrapId = scrapId + currentPage = page ) } } - fun updateInternDialogVisible(visibility: Boolean = false) { + fun updateListVisibility( + visibility: Boolean + ) { _uiState.update { currentState -> currentState.copy( - isInternshipClicked = visibility + isListEnabled = visibility ) } } - fun updateInternshipModel(scrapDetailModel: CalendarScrapDetail?) { + fun updateWeekVisibility( + visibility: Boolean + ) { _uiState.update { currentState -> currentState.copy( - internshipModel = scrapDetailModel + isWeekEnabled = visibility ) } } - - fun getScrapMonth( - year: Int, month: Int - ) = viewModelScope.launch { - withContext(Dispatchers.IO) { - calendarRepository.getScrapMonth(year, month) - }.fold( - onSuccess = { - _MonthUiState.update { currentState -> - currentState.copy( - loadState = UiState.Success(it) - ) - } - }, - onFailure = { - _calendarSideEffect.emit(CalendarSideEffect.ShowToast(R.string.server_failure)) - } - ) - } - - fun getScrapMonthList( - year: Int, month: Int - ) = viewModelScope.launch { - withContext(Dispatchers.IO) { - calendarRepository.getScrapMonthList(year, month) - }.fold( - onSuccess = { - _calendarListState.update { currentState -> - currentState.copy( - loadState = if (it.isNotEmpty()) UiState.Success(it) else UiState.Empty - //loadState = UiState.Success(it) - ) - } - }, - onFailure = { - _calendarListState.update { currentState -> - currentState.copy( - loadState = UiState.Failure(it.message.toString()) - ) - - } - _calendarSideEffect.emit(CalendarSideEffect.ShowToast(R.string.server_failure)) - } - ) - } - - fun getScrapWeekList() = viewModelScope.launch { - withContext(Dispatchers.IO) { - calendarRepository.getScrapDayList(_uiState.value.selectedDate) - }.fold( - onSuccess = { - _calendarWeekState.update { currentState -> - currentState.copy( - loadState = if (it.isNotEmpty()) UiState.Success(it) else UiState.Empty - ) - } - }, - onFailure = { - _calendarWeekState.update { currentState -> - currentState.copy( - loadState = UiState.Failure(it.message.toString()) - ) - - } - _calendarSideEffect.emit(CalendarSideEffect.ShowToast(R.string.server_failure)) - } - ) - } - - fun deleteScrap(isFromWeekScreen: Boolean = false) = viewModelScope.launch { - _calendarWeekState.value.loadState - .takeIf { it is UiState.Success } - ?.let { CalendarScrapRequest(_uiState.value.scrapId, null) }?.let { scrapRequestModel -> - scrapRepository.deleteScrap( - scrapRequestModel - ).onSuccess { - runCatching { - if (isFromWeekScreen) { - getScrapWeekList() - } else { - getScrapMonthList( - _uiState.value.selectedDate.year, - _uiState.value.selectedDate.monthValue - ) - } - }.onSuccess { - updateScrapCancelDialogVisible() - } - }.onFailure { - _calendarSideEffect.emit( - CalendarSideEffect.ShowToast(R.string.server_failure) - ) - } - } - } - - fun patchScrap(color: Color, isFromWeekScreen: Boolean = false) = viewModelScope.launch { - val scrapId = _uiState.value.internshipModel?.scrapId ?: 0 - val colorIndex = getColorIndex(color) - - scrapRepository.patchScrap(CalendarScrapRequest(scrapId, colorIndex)) - .onSuccess { - runCatching { - if (isFromWeekScreen) { - getScrapWeekList() - } else { - getScrapMonthList( - _uiState.value.selectedDate.year, - _uiState.value.selectedDate.monthValue - ) - } - } - }.onFailure { - _calendarSideEffect.emit(CalendarSideEffect.ShowToast(R.string.server_failure)) - } - } - - private fun getColorIndex(color: Color): Int = listOf( - CalRed, - CalOrange1, - CalOrange2, - CalYellow, - CalGreen1, - CalGreen2, - CalBlue1, - CalBlue2, - CalPurple, - CalPink - ).indexOf(color) - } \ No newline at end of file diff --git a/feature/src/main/java/com/terning/feature/calendar/calendar/component/CalendarDialog.kt b/feature/src/main/java/com/terning/feature/calendar/calendar/component/CalendarDialog.kt deleted file mode 100644 index 6e2078db3..000000000 --- a/feature/src/main/java/com/terning/feature/calendar/calendar/component/CalendarDialog.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.terning.feature.calendar.calendar.component - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.LocalLifecycleOwner -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController -import androidx.navigation.compose.rememberNavController -import com.terning.core.extension.getFullDateStringInKorean -import com.terning.feature.calendar.calendar.CalendarViewModel -import com.terning.feature.intern.navigation.navigateIntern - -@Composable -fun CalendarDialog( - isWeekScreen: Boolean, - navController: NavController = rememberNavController(), - viewModel: CalendarViewModel = hiltViewModel() -) { - val lifecycleOwner = LocalLifecycleOwner.current - val uiState by viewModel.uiState.collectAsStateWithLifecycle(lifecycleOwner) - - if (uiState.isScrapButtonClicked) { - CalendarCancelDialog( - onDismissRequest = { viewModel.updateScrapCancelDialogVisible() }, - onClickScrapCancel = { - viewModel.deleteScrap(isWeekScreen) - } - ) - } - - if (uiState.isInternshipClicked) { - CalendarDetailDialog( - deadline = uiState.selectedDate.getFullDateStringInKorean(), - scrapDetailModel = uiState.internshipModel, - onDismissRequest = {viewModel.updateInternDialogVisible(false)}, - onClickChangeColorButton = { newColor -> - viewModel.patchScrap(newColor, isWeekScreen) - }, - onClickNavigateButton = {announcementId -> - viewModel.updateInternDialogVisible(false) - navController.navigateIntern(announcementId = announcementId) - } - ) - } -} \ No newline at end of file diff --git a/feature/src/main/java/com/terning/feature/calendar/calendar/model/CalendarModel.kt b/feature/src/main/java/com/terning/feature/calendar/calendar/model/CalendarModel.kt index 38179b1d9..9d5c69e90 100644 --- a/feature/src/main/java/com/terning/feature/calendar/calendar/model/CalendarModel.kt +++ b/feature/src/main/java/com/terning/feature/calendar/calendar/model/CalendarModel.kt @@ -2,6 +2,7 @@ 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( @@ -19,10 +20,15 @@ class CalendarModel internal constructor( const val START_YEAR = 2020 const val END_YEAR = 2030 - fun getDateByPage(page: Int): LocalDate = LocalDate.of( + 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/src/main/java/com/terning/feature/calendar/calendar/model/CalendarUiState.kt b/feature/src/main/java/com/terning/feature/calendar/calendar/model/CalendarUiState.kt index 0e24c4baa..e4d75e7ef 100644 --- a/feature/src/main/java/com/terning/feature/calendar/calendar/model/CalendarUiState.kt +++ b/feature/src/main/java/com/terning/feature/calendar/calendar/model/CalendarUiState.kt @@ -1,14 +1,11 @@ package com.terning.feature.calendar.calendar.model -import com.terning.domain.entity.CalendarScrapDetail import java.time.LocalDate data class CalendarUiState( - val selectedDate: LocalDate, + val selectedDate: LocalDate = LocalDate.now(), + val calendarModel: CalendarModel = CalendarModel(), + val currentPage: Int = 0, val isListEnabled: Boolean = false, - val isWeekEnabled: Boolean = false, - val isScrapButtonClicked: Boolean = false, - val isInternshipClicked: Boolean = false, - val internshipModel: CalendarScrapDetail? = null, - val scrapId: Long = -1 + val isWeekEnabled: Boolean = false ) diff --git a/feature/src/main/java/com/terning/feature/calendar/calendar/navigation/CalendarNavigation.kt b/feature/src/main/java/com/terning/feature/calendar/calendar/navigation/CalendarNavigation.kt index dfa59c258..7f06fac41 100644 --- a/feature/src/main/java/com/terning/feature/calendar/calendar/navigation/CalendarNavigation.kt +++ b/feature/src/main/java/com/terning/feature/calendar/calendar/navigation/CalendarNavigation.kt @@ -8,6 +8,7 @@ import androidx.navigation.NavOptions import androidx.navigation.compose.composable import com.terning.core.navigation.MainTabRoute import com.terning.feature.calendar.calendar.CalendarRoute +import com.terning.feature.intern.navigation.navigateIntern import kotlinx.serialization.Serializable @@ -36,7 +37,8 @@ fun NavGraphBuilder.calendarNavGraph( } ) { CalendarRoute( - navController = navHostController + navigateUp = navHostController::navigateUp, + navigateToAnnouncement = navHostController::navigateIntern ) } } diff --git a/feature/src/main/java/com/terning/feature/calendar/list/CalendarListScreen.kt b/feature/src/main/java/com/terning/feature/calendar/list/CalendarListScreen.kt index 580d8105d..d5563b7ff 100644 --- a/feature/src/main/java/com/terning/feature/calendar/list/CalendarListScreen.kt +++ b/feature/src/main/java/com/terning/feature/calendar/list/CalendarListScreen.kt @@ -18,52 +18,103 @@ 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.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController -import androidx.navigation.compose.rememberNavController +import androidx.lifecycle.flowWithLifecycle import com.terning.core.designsystem.theme.Back 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.core.extension.getDateAsMapString +import com.terning.core.extension.getFullDateStringInKorean import com.terning.core.extension.isListNotEmpty +import com.terning.core.extension.toast import com.terning.core.state.UiState +import com.terning.domain.entity.CalendarScrapDetail import com.terning.feature.R -import com.terning.feature.calendar.calendar.CalendarViewModel -import com.terning.feature.calendar.calendar.component.CalendarDialog +import com.terning.feature.calendar.calendar.component.CalendarCancelDialog +import com.terning.feature.calendar.calendar.component.CalendarDetailDialog import com.terning.feature.calendar.calendar.model.CalendarDefaults.flingBehavior -import com.terning.feature.calendar.calendar.model.CalendarModel.Companion.getDateByPage +import com.terning.feature.calendar.calendar.model.CalendarModel.Companion.getLocalDateByPage import com.terning.feature.calendar.list.component.CalendarScrapList +import com.terning.feature.calendar.list.model.CalendarListUiState import kotlinx.coroutines.flow.distinctUntilChanged import java.time.LocalDate @Composable -fun CalendarListScreen( +fun CalendarListRoute( pages: Int, listState: LazyListState, modifier: Modifier = Modifier, - navController: NavController = rememberNavController(), - viewModel: CalendarViewModel = hiltViewModel() + navigateToAnnouncement: (Long) -> Unit, + viewModel: CalendarListViewModel = hiltViewModel() ) { val lifecycleOwner = LocalLifecycleOwner.current - val scrapState by viewModel.calendarListState.collectAsStateWithLifecycle(lifecycleOwner) + val uiState by viewModel.uiState.collectAsStateWithLifecycle(lifecycleOwner) + + val context = LocalContext.current + LaunchedEffect(viewModel.sideEffect, lifecycleOwner) { + viewModel.sideEffect.flowWithLifecycle(lifecycle = lifecycleOwner.lifecycle) + .collect { sideEffect -> + when (sideEffect) { + is CalendarListSideEffect.ShowToast -> context.toast(sideEffect.message) + } + } + } LaunchedEffect(key1 = listState) { snapshotFlow { listState.firstVisibleItemIndex } .distinctUntilChanged() .collect { val page = listState.firstVisibleItemIndex - val date = getDateByPage(page) - viewModel.getScrapMonthList(date.year, date.monthValue) + val date = getLocalDateByPage(page) + viewModel.updateCurrentDate(date) + viewModel.getScrapMonthList(date) } } + CalendarListScreen( + pages = pages, + listState = listState, + uiState = uiState, + modifier = modifier, + navigateToAnnouncement = navigateToAnnouncement, + onDismissCancelDialog = { viewModel.updateScrapCancelDialogVisibility(false) }, + onDismissInternDialog = { viewModel.updateInternDialogVisibility(false) }, + onClickChangeColor = { newColor -> viewModel.patchScrap(newColor) }, + onClickScrapCancel = { uiState.scrapId?.let { viewModel.deleteScrap(it) } }, + onClickScrapButton = { scrapId -> + viewModel.updateScrapId(scrapId) + viewModel.updateScrapCancelDialogVisibility(true) + }, + onClickInternship = { calendarScrapDetail -> + viewModel.updateInternshipModel(calendarScrapDetail) + viewModel.updateInternDialogVisibility(true) + } + ) +} + +@Composable +private fun CalendarListScreen( + pages: Int, + listState: LazyListState, + uiState: CalendarListUiState, + navigateToAnnouncement: (Long) -> Unit, + onDismissCancelDialog: () -> Unit, + onDismissInternDialog: () -> Unit, + onClickChangeColor: (Color) -> Unit, + onClickScrapCancel: () -> Unit, + onClickInternship: (CalendarScrapDetail) -> Unit, + onClickScrapButton: (Long) -> Unit, + modifier: Modifier = Modifier +) { Box { LazyRow( modifier = modifier @@ -75,7 +126,7 @@ fun CalendarListScreen( ) ) { items(pages) { page -> - val getDate = getDateByPage(page) + val getDate = getLocalDateByPage(page) LazyColumn( modifier = Modifier @@ -83,7 +134,7 @@ fun CalendarListScreen( .fillMaxHeight() .background(Back) ) { - when (scrapState.loadState) { + when (uiState.loadState) { UiState.Loading -> {} UiState.Empty -> { item { @@ -107,7 +158,7 @@ fun CalendarListScreen( is UiState.Failure -> {} is UiState.Success -> { items(getDate.lengthOfMonth()) { day -> - val scrapMap = (scrapState.loadState as UiState.Success).data + val scrapMap = uiState.loadState.data val currentDate = LocalDate.of(getDate.year, getDate.monthValue, day + 1) val dateIndex = currentDate.getDateAsMapString() @@ -116,13 +167,8 @@ fun CalendarListScreen( CalendarScrapList( selectedDate = currentDate, scrapList = scrapMap[dateIndex].orEmpty(), - onScrapButtonClicked = { scrapId -> - viewModel.updateScrapCancelDialogVisible(scrapId) - }, - onInternshipClicked = { scrapDetailModel -> - viewModel.updateInternshipModel(scrapDetailModel) - viewModel.updateInternDialogVisible(true) - }, + onScrapButtonClicked = onClickScrapButton, + onInternshipClicked = onClickInternship, isFromList = true ) @@ -140,11 +186,28 @@ fun CalendarListScreen( } } } + } + + if (uiState.scrapDialogVisibility) { + CalendarCancelDialog( + onDismissRequest = onDismissCancelDialog, + onClickScrapCancel = { + onClickScrapCancel() + onDismissCancelDialog() + } + ) + } - CalendarDialog( - isWeekScreen = false, - viewModel = viewModel, - navController = navController + if (uiState.internDialogVisibility) { + CalendarDetailDialog( + deadline = uiState.currentDate.getFullDateStringInKorean(), + scrapDetailModel = uiState.internshipModel, + onDismissRequest = onDismissInternDialog, + onClickChangeColorButton = onClickChangeColor, + onClickNavigateButton = { announcementId -> + navigateToAnnouncement(announcementId) + onDismissInternDialog() + } ) } } diff --git a/feature/src/main/java/com/terning/feature/calendar/list/CalendarListSideEffect.kt b/feature/src/main/java/com/terning/feature/calendar/list/CalendarListSideEffect.kt new file mode 100644 index 000000000..425fc06e4 --- /dev/null +++ b/feature/src/main/java/com/terning/feature/calendar/list/CalendarListSideEffect.kt @@ -0,0 +1,7 @@ +package com.terning.feature.calendar.list + +import androidx.annotation.StringRes + +sealed class CalendarListSideEffect { + data class ShowToast(@StringRes val message: Int) : CalendarListSideEffect() +} \ No newline at end of file diff --git a/feature/src/main/java/com/terning/feature/calendar/list/CalendarListViewModel.kt b/feature/src/main/java/com/terning/feature/calendar/list/CalendarListViewModel.kt new file mode 100644 index 000000000..a178a7f4d --- /dev/null +++ b/feature/src/main/java/com/terning/feature/calendar/list/CalendarListViewModel.kt @@ -0,0 +1,162 @@ +package com.terning.feature.calendar.list + +import androidx.compose.ui.graphics.Color +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.terning.core.designsystem.theme.CalBlue1 +import com.terning.core.designsystem.theme.CalBlue2 +import com.terning.core.designsystem.theme.CalGreen1 +import com.terning.core.designsystem.theme.CalGreen2 +import com.terning.core.designsystem.theme.CalOrange1 +import com.terning.core.designsystem.theme.CalOrange2 +import com.terning.core.designsystem.theme.CalPink +import com.terning.core.designsystem.theme.CalPurple +import com.terning.core.designsystem.theme.CalRed +import com.terning.core.designsystem.theme.CalYellow +import com.terning.core.state.UiState +import com.terning.domain.entity.CalendarScrapDetail +import com.terning.domain.entity.CalendarScrapRequest +import com.terning.domain.repository.CalendarRepository +import com.terning.domain.repository.ScrapRepository +import com.terning.feature.R +import com.terning.feature.calendar.list.model.CalendarListUiState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.time.LocalDate +import javax.inject.Inject + +@HiltViewModel +class CalendarListViewModel @Inject constructor( + private val calendarRepository: CalendarRepository, + private val scrapRepository: ScrapRepository +): ViewModel(){ + private val _uiState = MutableStateFlow(CalendarListUiState()) + val uiState = _uiState.asStateFlow() + + private val _sideEffect: MutableSharedFlow = MutableSharedFlow() + val sideEffect = _sideEffect.asSharedFlow() + + fun updateCurrentDate(date: LocalDate) { + _uiState.update { currentState -> + currentState.copy( + currentDate = date + ) + } + } + + fun updateScrapCancelDialogVisibility(visibility: Boolean) { + _uiState.update { currentState -> + currentState.copy( + scrapDialogVisibility = visibility + ) + } + } + + fun updateScrapId(scrapId: Long? = null) { + _uiState.update { currentState -> + currentState.copy( + scrapId = scrapId + ) + } + } + + fun updateInternDialogVisibility(visibility: Boolean) { + _uiState.update { currentState -> + currentState.copy( + internDialogVisibility = visibility + ) + } + } + + fun updateInternshipModel(scrapDetailModel: CalendarScrapDetail?) { + _uiState.update { currentState -> + currentState.copy( + internshipModel = scrapDetailModel + ) + } + } + + fun getScrapMonthList( + date: LocalDate + ) = viewModelScope.launch { + withContext(Dispatchers.IO) { + calendarRepository.getScrapMonthList( + year = _uiState.value.currentDate.year, + month = _uiState.value.currentDate.monthValue + ) + }.fold( + onSuccess = { + _uiState.update { currentState -> + currentState.copy( + loadState = if (it.isNotEmpty()) UiState.Success(it) else UiState.Empty + ) + } + }, + onFailure = { + _uiState.update { currentState -> + currentState.copy( + loadState = UiState.Failure(it.message.toString()) + ) + + } + _sideEffect.emit(CalendarListSideEffect.ShowToast(R.string.server_failure)) + } + ) + } + + fun deleteScrap( + scrapId: Long + ) = viewModelScope.launch { + _uiState.value.loadState + .takeIf { it is UiState.Success } + ?.let { CalendarScrapRequest(scrapId, null) }?.let { scrapRequestModel -> + scrapRepository.deleteScrap( + scrapRequestModel + ).onSuccess { + runCatching { + getScrapMonthList(_uiState.value.currentDate) + }.onSuccess { + updateScrapCancelDialogVisibility(false) + } + }.onFailure { + _sideEffect.emit(CalendarListSideEffect.ShowToast(R.string.server_failure)) + } + } + } + + fun patchScrap( + color: Color + ) = viewModelScope.launch { + val scrapId = _uiState.value.internshipModel?.scrapId ?: 0 + val colorIndex = getColorIndex(color) + + scrapRepository.patchScrap(CalendarScrapRequest(scrapId, colorIndex)) + .onSuccess { + runCatching { + getScrapMonthList(_uiState.value.currentDate) + } + }.onFailure { + _sideEffect.emit(CalendarListSideEffect.ShowToast(R.string.server_failure)) + } + } + + private fun getColorIndex(color: Color): Int = listOf( + CalRed, + CalOrange1, + CalOrange2, + CalYellow, + CalGreen1, + CalGreen2, + CalBlue1, + CalBlue2, + CalPurple, + CalPink + ).indexOf(color) +} \ No newline at end of file diff --git a/feature/src/main/java/com/terning/feature/calendar/list/component/CalendarScrapList.kt b/feature/src/main/java/com/terning/feature/calendar/list/component/CalendarScrapList.kt index ef614de38..d8e48179b 100644 --- a/feature/src/main/java/com/terning/feature/calendar/list/component/CalendarScrapList.kt +++ b/feature/src/main/java/com/terning/feature/calendar/list/component/CalendarScrapList.kt @@ -19,11 +19,12 @@ import com.terning.domain.entity.CalendarScrapDetail import java.time.LocalDate @Composable -fun CalendarScrapList( +internal fun CalendarScrapList( selectedDate: LocalDate, scrapList: List, onScrapButtonClicked: (Long) -> Unit, onInternshipClicked: (CalendarScrapDetail) -> Unit, + modifier: Modifier = Modifier, isFromList: Boolean = false ) { val scrollState = rememberScrollState() diff --git a/feature/src/main/java/com/terning/feature/calendar/list/model/CalendarListState.kt b/feature/src/main/java/com/terning/feature/calendar/list/model/CalendarListState.kt deleted file mode 100644 index 8378b0a14..000000000 --- a/feature/src/main/java/com/terning/feature/calendar/list/model/CalendarListState.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.terning.feature.calendar.list.model - -import com.terning.core.state.UiState -import com.terning.domain.entity.CalendarScrapDetail - -data class CalendarListState( - val loadState: UiState>> = UiState.Loading -) \ No newline at end of file diff --git a/feature/src/main/java/com/terning/feature/calendar/list/model/CalendarListUiState.kt b/feature/src/main/java/com/terning/feature/calendar/list/model/CalendarListUiState.kt new file mode 100644 index 000000000..6377463d7 --- /dev/null +++ b/feature/src/main/java/com/terning/feature/calendar/list/model/CalendarListUiState.kt @@ -0,0 +1,14 @@ +package com.terning.feature.calendar.list.model + +import com.terning.core.state.UiState +import com.terning.domain.entity.CalendarScrapDetail +import java.time.LocalDate + +data class CalendarListUiState( + val loadState: UiState>> = UiState.Loading, + val currentDate: LocalDate = LocalDate.now(), + val scrapDialogVisibility: Boolean = false, + val internDialogVisibility: Boolean = false, + val scrapId: Long? = null, + val internshipModel: CalendarScrapDetail? = null +) \ No newline at end of file diff --git a/feature/src/main/java/com/terning/feature/calendar/list/model/Scrap.kt b/feature/src/main/java/com/terning/feature/calendar/list/model/Scrap.kt deleted file mode 100644 index 288508692..000000000 --- a/feature/src/main/java/com/terning/feature/calendar/list/model/Scrap.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.terning.feature.calendar.list.model - -import androidx.compose.runtime.Immutable -import androidx.compose.ui.graphics.Color - -@Immutable -data class Scrap( - val text: String, - val backgroundColor: Color, - val dDay: String, - val period: String, - val isScraped: Boolean = true, - val image: String? = null -) diff --git a/feature/src/main/java/com/terning/feature/calendar/month/CalendarMonthScreen.kt b/feature/src/main/java/com/terning/feature/calendar/month/CalendarMonthScreen.kt index e9ac04400..609769fff 100644 --- a/feature/src/main/java/com/terning/feature/calendar/month/CalendarMonthScreen.kt +++ b/feature/src/main/java/com/terning/feature/calendar/month/CalendarMonthScreen.kt @@ -1,59 +1,130 @@ package com.terning.feature.calendar.month import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.LazyRow import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.flowWithLifecycle +import com.terning.core.extension.toast import com.terning.core.state.UiState -import com.terning.feature.calendar.calendar.CalendarViewModel -import com.terning.feature.calendar.calendar.model.CalendarModel.Companion.getDateByPage -import com.terning.feature.calendar.calendar.model.CalendarUiState -import com.terning.feature.calendar.month.component.HorizontalCalendar +import com.terning.domain.entity.CalendarScrap +import com.terning.feature.calendar.calendar.model.CalendarDefaults.flingBehavior +import com.terning.feature.calendar.calendar.model.CalendarModel.Companion.getLocalDateByPage +import com.terning.feature.calendar.month.model.MonthModel +import com.terning.feature.calendar.month.component.CalendarMonth +import com.terning.feature.calendar.month.model.CalendarMonthUiState import kotlinx.coroutines.flow.distinctUntilChanged +import java.time.LocalDate +import java.time.YearMonth @Composable -internal fun CalendarMonthScreen( - pages: Int, +fun CalendarMonthRoute( listState: LazyListState, - calendarUiState: CalendarUiState, + pages: Int, + selectedDate: LocalDate, + updateSelectedDate: (LocalDate) -> Unit, modifier: Modifier = Modifier, - viewModel: CalendarViewModel = hiltViewModel() + viewModel: CalendarMonthViewModel = hiltViewModel() ) { + val context = LocalContext.current val lifecycleOwner = LocalLifecycleOwner.current - val scrapState by viewModel.calendarMonthState.collectAsStateWithLifecycle(lifecycleOwner) + val monthUiState by viewModel.uiState.collectAsStateWithLifecycle(lifecycleOwner) + + LaunchedEffect(viewModel.sideEffect, lifecycleOwner) { + viewModel.sideEffect.flowWithLifecycle(lifecycle = lifecycleOwner.lifecycle) + .collect { sideEffect -> + when (sideEffect) { + is CalendarMonthSideEffect.ShowToast -> context.toast(sideEffect.message) + } + } + } LaunchedEffect(key1 = listState) { snapshotFlow { listState.firstVisibleItemIndex } .distinctUntilChanged() .collect { val page = listState.firstVisibleItemIndex - val date = getDateByPage(page) + val date = getLocalDateByPage(page) viewModel.getScrapMonth(date.year, date.monthValue) } } - when (scrapState.loadState) { + CalendarMonthScreen( + selectedDate = selectedDate, + calendarMonthUiState = monthUiState, + listState = listState, + pages = pages, + updateSelectedDate = updateSelectedDate, + modifier = modifier + ) +} + +@Composable +private fun CalendarMonthScreen( + listState: LazyListState, + calendarMonthUiState: CalendarMonthUiState, + pages: Int, + selectedDate: LocalDate, + updateSelectedDate: (LocalDate) -> Unit, + modifier: Modifier = Modifier, +) { + when (calendarMonthUiState.loadState) { UiState.Loading -> {} UiState.Empty -> {} is UiState.Failure -> {} is UiState.Success -> { - val scrapMap = (scrapState.loadState as UiState.Success).data - HorizontalCalendar( + val scrapMap = calendarMonthUiState.loadState.data + + MonthSuccessScreen( pages = pages, listState = listState, - isWeekEnabled = calendarUiState.isWeekEnabled, scrapMap = scrapMap, - onDateSelected = { viewModel.updateSelectedDate(it) }, - selectedDate = calendarUiState.selectedDate, + onDateSelected = updateSelectedDate, + selectedDate = selectedDate, modifier = modifier ) } } } +@Composable +private fun MonthSuccessScreen( + pages: Int, + listState: LazyListState, + selectedDate: LocalDate, + scrapMap: Map>, + onDateSelected: (LocalDate) -> Unit, + modifier: Modifier = Modifier, +) { + LazyRow( + modifier = modifier, + state = listState, + userScrollEnabled = true, + flingBehavior = flingBehavior( + state = listState + ) + ) { + items(pages) { page -> + val date = getLocalDateByPage(page) + val monthModel = MonthModel(YearMonth.of(date.year, date.month)) + + CalendarMonth( + modifier = Modifier.fillParentMaxSize(), + onDateSelected = onDateSelected, + monthModel = monthModel, + scrapMap = scrapMap, + selectedDate = selectedDate, + isWeekEnabled = false + ) + } + } +} + diff --git a/feature/src/main/java/com/terning/feature/calendar/month/CalendarMonthSideEffect.kt b/feature/src/main/java/com/terning/feature/calendar/month/CalendarMonthSideEffect.kt new file mode 100644 index 000000000..77c1f858e --- /dev/null +++ b/feature/src/main/java/com/terning/feature/calendar/month/CalendarMonthSideEffect.kt @@ -0,0 +1,7 @@ +package com.terning.feature.calendar.month + +import androidx.annotation.StringRes + +sealed class CalendarMonthSideEffect { + data class ShowToast(@StringRes val message: Int) : CalendarMonthSideEffect() +} \ No newline at end of file diff --git a/feature/src/main/java/com/terning/feature/calendar/month/CalendarMonthViewModel.kt b/feature/src/main/java/com/terning/feature/calendar/month/CalendarMonthViewModel.kt new file mode 100644 index 000000000..0f17cecd7 --- /dev/null +++ b/feature/src/main/java/com/terning/feature/calendar/month/CalendarMonthViewModel.kt @@ -0,0 +1,48 @@ +package com.terning.feature.calendar.month + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.terning.core.state.UiState +import com.terning.domain.repository.CalendarRepository +import com.terning.feature.R +import com.terning.feature.calendar.month.model.CalendarMonthUiState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import javax.inject.Inject + +@HiltViewModel +class CalendarMonthViewModel @Inject constructor( + private val calendarRepository: CalendarRepository +): ViewModel() { + private val _uiState = MutableStateFlow(CalendarMonthUiState()) + val uiState = _uiState.asStateFlow() + + private val _sideEffect: MutableSharedFlow = MutableSharedFlow() + val sideEffect = _sideEffect.asSharedFlow() + + fun getScrapMonth( + year: Int, month: Int + ) = viewModelScope.launch { + withContext(Dispatchers.IO) { + calendarRepository.getScrapMonth(year, month) + }.fold( + onSuccess = { + _uiState.update { currentState -> + currentState.copy( + loadState = UiState.Success(it) + ) + } + }, + onFailure = { + _sideEffect.emit(CalendarMonthSideEffect.ShowToast(R.string.server_failure)) + } + ) + } +} \ No newline at end of file diff --git a/feature/src/main/java/com/terning/feature/calendar/month/component/HorizontalCalendar.kt b/feature/src/main/java/com/terning/feature/calendar/month/component/HorizontalCalendar.kt deleted file mode 100644 index 77a073e01..000000000 --- a/feature/src/main/java/com/terning/feature/calendar/month/component/HorizontalCalendar.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.terning.feature.calendar.month.component - -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import com.terning.domain.entity.CalendarScrap -import com.terning.feature.calendar.calendar.model.CalendarDefaults.flingBehavior -import com.terning.feature.calendar.calendar.model.CalendarModel.Companion.getDateByPage -import com.terning.feature.calendar.month.model.MonthModel -import java.time.LocalDate -import java.time.YearMonth - -@Composable -internal fun HorizontalCalendar( - pages: Int, - listState: LazyListState, - isWeekEnabled: Boolean, - selectedDate: LocalDate, - scrapMap: Map>, - onDateSelected: (LocalDate) -> Unit, - modifier: Modifier = Modifier, -) { - LazyRow( - modifier = modifier, - state = listState, - userScrollEnabled = true, - flingBehavior = flingBehavior( - state = listState - ) - ) { - items(pages) { page -> - val date = getDateByPage(page) - val monthModel = MonthModel(YearMonth.of(date.year, date.month)) - - CalendarMonth( - modifier = Modifier.fillParentMaxSize(), - onDateSelected = onDateSelected, - monthModel = monthModel, - scrapMap = scrapMap, - selectedDate = selectedDate, - isWeekEnabled = isWeekEnabled - ) - } - } -} \ No newline at end of file diff --git a/feature/src/main/java/com/terning/feature/calendar/month/model/MonthUiState.kt b/feature/src/main/java/com/terning/feature/calendar/month/model/CalendarMonthUiState.kt similarity index 86% rename from feature/src/main/java/com/terning/feature/calendar/month/model/MonthUiState.kt rename to feature/src/main/java/com/terning/feature/calendar/month/model/CalendarMonthUiState.kt index 195c93232..9734222ee 100644 --- a/feature/src/main/java/com/terning/feature/calendar/month/model/MonthUiState.kt +++ b/feature/src/main/java/com/terning/feature/calendar/month/model/CalendarMonthUiState.kt @@ -3,6 +3,6 @@ package com.terning.feature.calendar.month.model import com.terning.core.state.UiState import com.terning.domain.entity.CalendarScrap -data class MonthUiState( +data class CalendarMonthUiState( val loadState: UiState>> = UiState.Loading ) \ No newline at end of file diff --git a/feature/src/main/java/com/terning/feature/calendar/month/model/MonthModel.kt b/feature/src/main/java/com/terning/feature/calendar/month/model/MonthModel.kt index 6106a7863..f9b940cc9 100644 --- a/feature/src/main/java/com/terning/feature/calendar/month/model/MonthModel.kt +++ b/feature/src/main/java/com/terning/feature/calendar/month/model/MonthModel.kt @@ -1,6 +1,7 @@ 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 /** diff --git a/feature/src/main/java/com/terning/feature/calendar/week/CalendarWeekScreen.kt b/feature/src/main/java/com/terning/feature/calendar/week/CalendarWeekScreen.kt index 8ed45dd1a..44ee9d65c 100644 --- a/feature/src/main/java/com/terning/feature/calendar/week/CalendarWeekScreen.kt +++ b/feature/src/main/java/com/terning/feature/calendar/week/CalendarWeekScreen.kt @@ -15,100 +15,166 @@ import androidx.compose.runtime.getValue 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.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController -import androidx.navigation.compose.rememberNavController +import androidx.lifecycle.flowWithLifecycle import com.terning.core.designsystem.theme.Back import com.terning.core.designsystem.theme.Grey400 import com.terning.core.designsystem.theme.TerningTheme import com.terning.core.designsystem.theme.White +import com.terning.core.extension.getFullDateStringInKorean +import com.terning.core.extension.toast import com.terning.core.state.UiState import com.terning.domain.entity.CalendarScrapDetail import com.terning.feature.R +import com.terning.feature.calendar.calendar.component.CalendarCancelDialog +import com.terning.feature.calendar.calendar.component.CalendarDetailDialog import com.terning.feature.calendar.calendar.model.CalendarUiState -import com.terning.feature.calendar.calendar.CalendarViewModel -import com.terning.feature.calendar.calendar.component.CalendarDialog import com.terning.feature.calendar.list.component.CalendarScrapList import com.terning.feature.calendar.week.component.HorizontalCalendarWeek +import com.terning.feature.calendar.week.model.CalendarWeekUiState +import okhttp3.internal.toImmutableList import java.time.LocalDate @Composable -fun CalendarWeekScreen( - modifier: Modifier = Modifier, +fun CalendarWeekRoute( calendarUiState: CalendarUiState, - navController: NavController = rememberNavController(), - viewModel: CalendarViewModel = hiltViewModel() + navigateToAnnouncement: (Long) -> Unit, + updateSelectedDate: (LocalDate) -> Unit, + modifier: Modifier = Modifier, + viewModel: CalendarWeekViewModel = hiltViewModel() ) { val lifecycleOwner = LocalLifecycleOwner.current - val calendarWeekState by viewModel.calendarWeekState.collectAsStateWithLifecycle(lifecycleOwner) + val uiState by viewModel.uiState.collectAsStateWithLifecycle(lifecycleOwner) - LaunchedEffect(calendarUiState.selectedDate) { - viewModel.getScrapWeekList() + val context = LocalContext.current + LaunchedEffect(viewModel.sideEffect, lifecycleOwner) { + viewModel.sideEffect.flowWithLifecycle(lifecycle = lifecycleOwner.lifecycle) + .collect { sideEffect -> + when (sideEffect) { + is CalendarWeekSideEffect.ShowToast -> context.toast(sideEffect.message) + } + } } - Box { - Column( - modifier = modifier - .background(Back) + LaunchedEffect(key1 = calendarUiState.selectedDate) { + viewModel.updateSelectedDate(selectedDate = calendarUiState.selectedDate) + } + + LaunchedEffect(key1 = uiState.selectedDate) { + viewModel.getScrapWeekList(selectedDate = uiState.selectedDate) + } + + CalendarWeekScreen( + modifier = modifier, + uiState = uiState, + selectedDate = calendarUiState.selectedDate, + updateSelectedDate = updateSelectedDate, + navigateToAnnouncement = navigateToAnnouncement, + onDismissCancelDialog = { viewModel.updateScrapCancelDialogVisibility(false) }, + onDismissInternDialog = { viewModel.updateInternDialogVisibility(false) }, + onClickChangeColor = { viewModel.patchScrap(it) }, + onClickScrapCancel = { uiState.scrapId?.let { viewModel.deleteScrap(it) } }, + onClickScrapButton = {scrapId -> + with(viewModel) { + updateScrapId(scrapId) + updateScrapCancelDialogVisibility(true) + } + }, + onClickInternship = { scrapDetail -> + with(viewModel) { + updateInternDialogVisibility(true) + updateInternshipModel(scrapDetail) + } + }, + ) +} + +@Composable +private fun CalendarWeekScreen( + uiState: CalendarWeekUiState, + selectedDate: LocalDate, + updateSelectedDate: (LocalDate) -> Unit, + onDismissCancelDialog: () -> Unit, + onDismissInternDialog: () -> Unit, + onClickChangeColor: (Color) -> Unit, + onClickScrapCancel: () -> Unit, + onClickInternship: (CalendarScrapDetail) -> Unit, + onClickScrapButton: (Long) -> Unit, + navigateToAnnouncement: (Long) -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .background(Back) + ) { + Card( + modifier = Modifier + .shadow( + shape = RoundedCornerShape(bottomStart = 20.dp, bottomEnd = 20.dp), + elevation = 1.dp + ), + + shape = RoundedCornerShape(bottomStart = 20.dp, bottomEnd = 20.dp), ) { - Card( + HorizontalCalendarWeek( + selectedDate = selectedDate, + onDateSelected = updateSelectedDate, modifier = Modifier - .shadow( - shape = RoundedCornerShape(bottomStart = 20.dp, bottomEnd = 20.dp), - elevation = 1.dp - ), - - shape = RoundedCornerShape(bottomStart = 20.dp, bottomEnd = 20.dp), - ) { - HorizontalCalendarWeek( - modifier = Modifier - .fillMaxWidth() - .background(White), - calendarUiState = calendarUiState, - onDateSelected = { - viewModel.updateSelectedDate(it) - } + .fillMaxWidth() + .background(White) + ) + } + + when (uiState.loadState) { + is UiState.Loading -> {} + is UiState.Empty -> { + CalendarWeekEmpty() + } + is UiState.Failure -> {} + is UiState.Success -> { + CalendarWeekSuccess( + scrapList = uiState.loadState.data.toImmutableList(), + selectedDate = uiState.selectedDate, + onScrapButtonClicked = onClickScrapButton, + onInternshipClicked = onClickInternship ) } + } + } - when (calendarWeekState.loadState) { - is UiState.Loading -> {} - is UiState.Empty -> { - CalendarWeekEmpty() - } - - is UiState.Failure -> {} - is UiState.Success -> { - val scrapList = (calendarWeekState.loadState as UiState.Success).data - CalendarWeekSuccess( - scrapList = scrapList, - selectedDate = calendarUiState.selectedDate, - onScrapButtonClicked = { scrapId -> - viewModel.updateScrapCancelDialogVisible(scrapId) - }, - onInternshipClicked = { scrapDetailModel -> - viewModel.updateInternshipModel(scrapDetailModel) - viewModel.updateInternDialogVisible(true) - }) - } + if (uiState.scrapDialogVisibility) { + CalendarCancelDialog( + onDismissRequest = onDismissCancelDialog, + onClickScrapCancel = { + onClickScrapCancel() + onDismissCancelDialog() } - } + ) + } - CalendarDialog( - isWeekScreen = true, - viewModel = viewModel, - navController = navController + if (uiState.internDialogVisibility) { + CalendarDetailDialog( + deadline = uiState.selectedDate.getFullDateStringInKorean(), + scrapDetailModel = uiState.internshipModel, + onDismissRequest = onDismissInternDialog, + onClickChangeColorButton = onClickChangeColor, + onClickNavigateButton = { announcementId -> + navigateToAnnouncement(announcementId) + onDismissInternDialog() + } ) } } @Composable -fun CalendarWeekEmpty( +private fun CalendarWeekEmpty( modifier: Modifier = Modifier ) { Box( @@ -128,11 +194,11 @@ fun CalendarWeekEmpty( } @Composable -fun CalendarWeekSuccess( +private fun CalendarWeekSuccess( scrapList: List, onScrapButtonClicked: (Long) -> Unit, onInternshipClicked: (CalendarScrapDetail) -> Unit, - selectedDate: LocalDate, + selectedDate: LocalDate ) { CalendarScrapList( selectedDate = selectedDate, diff --git a/feature/src/main/java/com/terning/feature/calendar/week/CalendarWeekSideEffect.kt b/feature/src/main/java/com/terning/feature/calendar/week/CalendarWeekSideEffect.kt new file mode 100644 index 000000000..6ad6eedc0 --- /dev/null +++ b/feature/src/main/java/com/terning/feature/calendar/week/CalendarWeekSideEffect.kt @@ -0,0 +1,7 @@ +package com.terning.feature.calendar.week + +import androidx.annotation.StringRes + +sealed class CalendarWeekSideEffect { + data class ShowToast(@StringRes val message: Int) : CalendarWeekSideEffect() +} \ No newline at end of file diff --git a/feature/src/main/java/com/terning/feature/calendar/week/CalendarWeekViewModel.kt b/feature/src/main/java/com/terning/feature/calendar/week/CalendarWeekViewModel.kt new file mode 100644 index 000000000..07337f7db --- /dev/null +++ b/feature/src/main/java/com/terning/feature/calendar/week/CalendarWeekViewModel.kt @@ -0,0 +1,155 @@ +package com.terning.feature.calendar.week + +import androidx.compose.ui.graphics.Color +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.terning.core.designsystem.theme.CalBlue1 +import com.terning.core.designsystem.theme.CalBlue2 +import com.terning.core.designsystem.theme.CalGreen1 +import com.terning.core.designsystem.theme.CalGreen2 +import com.terning.core.designsystem.theme.CalOrange1 +import com.terning.core.designsystem.theme.CalOrange2 +import com.terning.core.designsystem.theme.CalPink +import com.terning.core.designsystem.theme.CalPurple +import com.terning.core.designsystem.theme.CalRed +import com.terning.core.designsystem.theme.CalYellow +import com.terning.core.state.UiState +import com.terning.domain.entity.CalendarScrapDetail +import com.terning.domain.entity.CalendarScrapRequest +import com.terning.domain.repository.CalendarRepository +import com.terning.domain.repository.ScrapRepository +import com.terning.feature.R +import com.terning.feature.calendar.week.model.CalendarWeekUiState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.time.LocalDate +import javax.inject.Inject + +@HiltViewModel +class CalendarWeekViewModel @Inject constructor( + private val calendarRepository: CalendarRepository, + private val scrapRepository: ScrapRepository +): ViewModel() { + private val _uiState = MutableStateFlow(CalendarWeekUiState()) + val uiState = _uiState.asStateFlow() + + private val _sideEffect: MutableSharedFlow = MutableSharedFlow() + val sideEffect = _sideEffect.asSharedFlow() + + fun updateSelectedDate(selectedDate: LocalDate) { + _uiState.update { currentState -> + currentState.copy( + selectedDate = selectedDate + ) + } + } + + fun updateScrapCancelDialogVisibility(visibility: Boolean) { + _uiState.update { currentState -> + currentState.copy( + scrapDialogVisibility = visibility + ) + } + } + + fun updateScrapId(scrapId: Long? = null) { + _uiState.update { currentState -> + currentState.copy( + scrapId = scrapId + ) + } + } + + fun updateInternDialogVisibility(visibility: Boolean) { + _uiState.update { currentState -> + currentState.copy( + internDialogVisibility = visibility + ) + } + } + + fun updateInternshipModel(scrapDetailModel: CalendarScrapDetail?) { + _uiState.update { currentState -> + currentState.copy( + internshipModel = scrapDetailModel + ) + } + } + + fun getScrapWeekList(selectedDate: LocalDate) = viewModelScope.launch { + withContext(Dispatchers.IO) { + calendarRepository.getScrapDayList(selectedDate) + }.fold( + onSuccess = { + _uiState.update { currentState -> + currentState.copy( + loadState = if (it.isNotEmpty()) UiState.Success(it) else UiState.Empty + ) + } + }, + onFailure = { + _uiState.update { currentState -> + currentState.copy( + loadState = UiState.Failure(it.message.toString()) + ) + + } + _sideEffect.emit(CalendarWeekSideEffect.ShowToast(R.string.server_failure)) + } + ) + } + + fun deleteScrap(scrapId: Long) = viewModelScope.launch { + _uiState.value.loadState + .takeIf { it is UiState.Success } + ?.let { CalendarScrapRequest(scrapId, null) }?.let { scrapRequestModel -> + scrapRepository.deleteScrap( + scrapRequestModel + ).onSuccess { + runCatching { + getScrapWeekList(selectedDate = _uiState.value.selectedDate) + }.onSuccess { + updateScrapCancelDialogVisibility(false) + } + }.onFailure { + _sideEffect.emit( + CalendarWeekSideEffect.ShowToast(R.string.server_failure) + ) + } + } + } + + fun patchScrap(color: Color) = viewModelScope.launch { + val scrapId = _uiState.value.internshipModel?.scrapId ?: 0 + val colorIndex = getColorIndex(color) + + scrapRepository.patchScrap(CalendarScrapRequest(scrapId, colorIndex)) + .onSuccess { + runCatching { + getScrapWeekList(selectedDate = _uiState.value.selectedDate) + } + }.onFailure { + _sideEffect.emit(CalendarWeekSideEffect.ShowToast(R.string.server_failure)) + } + } + + private fun getColorIndex(color: Color): Int = listOf( + CalRed, + CalOrange1, + CalOrange2, + CalYellow, + CalGreen1, + CalGreen2, + CalBlue1, + CalBlue2, + CalPurple, + CalPink + ).indexOf(color) +} \ No newline at end of file diff --git a/feature/src/main/java/com/terning/feature/calendar/week/component/HorizontalCalendarWeek.kt b/feature/src/main/java/com/terning/feature/calendar/week/component/HorizontalCalendarWeek.kt index 03d7aa8a4..593a927dd 100644 --- a/feature/src/main/java/com/terning/feature/calendar/week/component/HorizontalCalendarWeek.kt +++ b/feature/src/main/java/com/terning/feature/calendar/week/component/HorizontalCalendarWeek.kt @@ -12,7 +12,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.terning.core.extension.getWeekIndexContainingSelectedDate import com.terning.core.extension.isToday -import com.terning.feature.calendar.calendar.model.CalendarUiState import com.terning.feature.calendar.calendar.component.CalendarDay import com.terning.feature.calendar.month.model.MonthModel import java.time.LocalDate @@ -20,13 +19,12 @@ import java.time.YearMonth @Composable fun HorizontalCalendarWeek( - calendarUiState: CalendarUiState, - modifier: Modifier = Modifier, - onDateSelected: (LocalDate) -> Unit = {} + selectedDate: LocalDate, + onDateSelected: (LocalDate) -> Unit, + modifier: Modifier = Modifier ) { - val date = calendarUiState.selectedDate - val monthModel = MonthModel(YearMonth.of(date.year, date.monthValue)) - val currentWeek = date.getWeekIndexContainingSelectedDate(monthModel.inDays) + val monthModel = MonthModel(YearMonth.of(selectedDate.year, selectedDate.monthValue)) + val currentWeek = selectedDate.getWeekIndexContainingSelectedDate(monthModel.inDays) val pagerState = rememberPagerState( initialPage = currentWeek, @@ -46,7 +44,7 @@ fun HorizontalCalendarWeek( items(items = monthModel.calendarMonth.weekDays[page]) { day -> CalendarDay( dayData = day, - isSelected = calendarUiState.selectedDate == day.date && calendarUiState.isWeekEnabled, + isSelected = day.date == selectedDate, isToday = day.date.isToday(), onDateSelected = onDateSelected ) diff --git a/feature/src/main/java/com/terning/feature/calendar/week/model/CalendarWeekState.kt b/feature/src/main/java/com/terning/feature/calendar/week/model/CalendarWeekState.kt deleted file mode 100644 index 054e0bb1c..000000000 --- a/feature/src/main/java/com/terning/feature/calendar/week/model/CalendarWeekState.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.terning.feature.calendar.week.model - -import com.terning.core.state.UiState -import com.terning.domain.entity.CalendarScrapDetail - -data class CalendarWeekState( - val loadState: UiState> = UiState.Loading, -) \ No newline at end of file diff --git a/feature/src/main/java/com/terning/feature/calendar/week/model/CalendarWeekUiState.kt b/feature/src/main/java/com/terning/feature/calendar/week/model/CalendarWeekUiState.kt new file mode 100644 index 000000000..b878bbb2f --- /dev/null +++ b/feature/src/main/java/com/terning/feature/calendar/week/model/CalendarWeekUiState.kt @@ -0,0 +1,14 @@ +package com.terning.feature.calendar.week.model + +import com.terning.core.state.UiState +import com.terning.domain.entity.CalendarScrapDetail +import java.time.LocalDate + +data class CalendarWeekUiState( + val loadState: UiState> = UiState.Loading, + val selectedDate: LocalDate = LocalDate.now(), + val scrapDialogVisibility: Boolean = false, + val internDialogVisibility: Boolean = false, + val scrapId: Long? = null, + val internshipModel: CalendarScrapDetail? = null +) \ No newline at end of file