diff --git a/data/src/main/java/com/sopt/data/dto/response/ResponseGetCalendarDto.kt b/data/src/main/java/com/sopt/data/dto/response/ResponseGetCalendarDto.kt index cb0fac86..bf231c6f 100644 --- a/data/src/main/java/com/sopt/data/dto/response/ResponseGetCalendarDto.kt +++ b/data/src/main/java/com/sopt/data/dto/response/ResponseGetCalendarDto.kt @@ -19,7 +19,8 @@ data class CalendarAppointmentDayDto( @Serializable data class CalendarAppointmentDto( - @SerialName("id") val id: Long, + @SerialName("appointmentId") val appointmentId: Long, + @SerialName("optionId") val optionId: Long, @SerialName("name") val name: String, @SerialName("date") val date: String, @SerialName("startTime") val startTime: String, diff --git a/data/src/main/java/com/sopt/data/dto/response/ResponseGetTimeTableDto.kt b/data/src/main/java/com/sopt/data/dto/response/ResponseGetTimeTableDto.kt index 0b0541de..e5e8a824 100644 --- a/data/src/main/java/com/sopt/data/dto/response/ResponseGetTimeTableDto.kt +++ b/data/src/main/java/com/sopt/data/dto/response/ResponseGetTimeTableDto.kt @@ -12,6 +12,7 @@ data class ResponseGetTimeTableDto( @Serializable data class ResponseAppointmentScheduleDto( + @SerialName("duration") val duration: Long, @SerialName("appointmentHostSelectionTimes") val appointmentHostSelectionTimes: List, @SerialName("appointmentMembersInfo") val appointmentMembersInfo: List ) diff --git a/data/src/main/java/com/sopt/data/mapper/ResponseGetCalendarDtoMapper.kt b/data/src/main/java/com/sopt/data/mapper/ResponseGetCalendarDtoMapper.kt index cd654955..b95c9da7 100644 --- a/data/src/main/java/com/sopt/data/mapper/ResponseGetCalendarDtoMapper.kt +++ b/data/src/main/java/com/sopt/data/mapper/ResponseGetCalendarDtoMapper.kt @@ -20,7 +20,8 @@ fun CalendarAppointmentDayDto.toCalendarAppointmentDayEntity() = CalendarAppoint ) fun CalendarAppointmentDto.toCalendarAppointmentEntity() = CalendarAppointmentEntity( - id = id, + appointmentId = appointmentId, + optionId = optionId, name = name, date = date, startTime = startTime, diff --git a/data/src/main/java/com/sopt/data/mapper/ResponseGetTimeTableDtoMapper.kt b/data/src/main/java/com/sopt/data/mapper/ResponseGetTimeTableDtoMapper.kt index 56b6ad1c..2ced5000 100644 --- a/data/src/main/java/com/sopt/data/mapper/ResponseGetTimeTableDtoMapper.kt +++ b/data/src/main/java/com/sopt/data/mapper/ResponseGetTimeTableDtoMapper.kt @@ -15,6 +15,7 @@ fun ResponseGetTimeTableDto.toTimeTableEntity() = TimeTableEntity( ) fun ResponseAppointmentScheduleDto.toAppointmentScheduleEntity() = AppointmentScheduleEntity( + duration = duration, appointmentHostSelectionTimes = appointmentHostSelectionTimes.map { it.toTimeEntity() }, appointmentMembersInfo = appointmentMembersInfo.map { it?.toAppointmentMembersInfoEntity() ?: AppointmentMembersInfoEntity( diff --git a/domain/src/main/java/com/sopt/domain/entity/CalendarEntity.kt b/domain/src/main/java/com/sopt/domain/entity/CalendarEntity.kt index 7d20334e..de5f89f7 100644 --- a/domain/src/main/java/com/sopt/domain/entity/CalendarEntity.kt +++ b/domain/src/main/java/com/sopt/domain/entity/CalendarEntity.kt @@ -13,7 +13,8 @@ data class CalendarAppointmentDayEntity( ) data class CalendarAppointmentEntity( - val id: Long, + val appointmentId: Long, + val optionId: Long, val name: String, val date: String, val startTime: String, diff --git a/domain/src/main/java/com/sopt/domain/entity/TimeTableEntity.kt b/domain/src/main/java/com/sopt/domain/entity/TimeTableEntity.kt index 87a32611..5888c1db 100644 --- a/domain/src/main/java/com/sopt/domain/entity/TimeTableEntity.kt +++ b/domain/src/main/java/com/sopt/domain/entity/TimeTableEntity.kt @@ -8,6 +8,7 @@ data class TimeTableEntity( ) data class AppointmentScheduleEntity( + val duration: Long, val appointmentHostSelectionTimes: List, val appointmentMembersInfo: List ) diff --git a/presentation/src/main/java/com/sopt/presentation/appointment/AppointmentRoute.kt b/presentation/src/main/java/com/sopt/presentation/appointment/AppointmentRoute.kt index 357a7636..166618fd 100644 --- a/presentation/src/main/java/com/sopt/presentation/appointment/AppointmentRoute.kt +++ b/presentation/src/main/java/com/sopt/presentation/appointment/AppointmentRoute.kt @@ -69,7 +69,7 @@ fun AppointmentRoute( appointmentId: Long, appointmentName: String, navigateUp: () -> Unit, - navigateToAppointmentCheck: (Long, Long, String, List) -> Unit, + navigateToAppointmentCheck: (Long, Long, String, List, Long) -> Unit, navigateToAppointmentConfirm: (Long, Long, Long, String, Boolean) -> Unit, appointmentViewModel: AppointmentViewModel = hiltViewModel() ) { @@ -85,7 +85,8 @@ fun AppointmentRoute( sideEffect.groupId, sideEffect.appointmentsId, sideEffect.appointmentName, - sideEffect.availablePeriods + sideEffect.availablePeriods, + sideEffect.duration ) } @@ -122,7 +123,8 @@ fun AppointmentRoute( groupId, appointmentId, appointmentName, - (getTimeTableState as UiState.Success).data.appointmentSchedule.appointmentHostSelectionTimes + (getTimeTableState as UiState.Success).data.appointmentSchedule.appointmentHostSelectionTimes, + (getTimeTableState as UiState.Success).data.appointmentSchedule.duration ) } } diff --git a/presentation/src/main/java/com/sopt/presentation/appointment/AppointmentSideEffect.kt b/presentation/src/main/java/com/sopt/presentation/appointment/AppointmentSideEffect.kt index f3ad0ef7..02500b26 100644 --- a/presentation/src/main/java/com/sopt/presentation/appointment/AppointmentSideEffect.kt +++ b/presentation/src/main/java/com/sopt/presentation/appointment/AppointmentSideEffect.kt @@ -8,7 +8,8 @@ sealed class AppointmentSideEffect { val groupId: Long, val appointmentsId: Long, val appointmentName: String, - val availablePeriods: List + val availablePeriods: List, + val duration: Long ) : AppointmentSideEffect() data class NavigateToAppointmentConfirm( diff --git a/presentation/src/main/java/com/sopt/presentation/appointment/appointmentCheck/AppointmentCheckRoute.kt b/presentation/src/main/java/com/sopt/presentation/appointment/appointmentCheck/AppointmentCheckRoute.kt index a06c5382..c2b5faed 100644 --- a/presentation/src/main/java/com/sopt/presentation/appointment/appointmentCheck/AppointmentCheckRoute.kt +++ b/presentation/src/main/java/com/sopt/presentation/appointment/appointmentCheck/AppointmentCheckRoute.kt @@ -1,5 +1,8 @@ package com.sopt.presentation.appointment.appointmentCheck +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize @@ -8,15 +11,20 @@ import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign @@ -27,12 +35,16 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.sopt.core.designsystem.component.button.NoostakBottomButton import com.sopt.core.designsystem.component.dialog.NoostakDialog +import com.sopt.core.designsystem.component.snackbar.NoostakSnackBar +import com.sopt.core.designsystem.component.snackbar.SNACK_BAR_DURATION import com.sopt.core.designsystem.component.timetable.NoostakEditableTimeTable import com.sopt.core.designsystem.component.topappbar.NoostakTopAppBar import com.sopt.core.designsystem.theme.NoostakAndroidTheme import com.sopt.core.designsystem.theme.NoostakTheme import com.sopt.domain.entity.TimeEntity import com.sopt.presentation.R +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import timber.log.Timber @Composable @@ -41,14 +53,30 @@ fun AppointmentCheckRoute( appointmentId: Long, appointmentName: String, availablePeriods: List, + duration: Long, navigateUp: () -> Unit, navigateToAppointment: (Long, Long, String) -> Unit, navigateToGroupDetail: (Long) -> Unit, appointmentCheckViewModel: AppointmentCheckViewModel = hiltViewModel() ) { + val context = LocalContext.current val showErrorDialog by appointmentCheckViewModel.showErrorDialog.collectAsStateWithLifecycle() var selectedData by remember { mutableStateOf(emptyList()) } val rememberedAvailablePeriods = remember { availablePeriods } + val snackBarHostState = remember { SnackbarHostState() } + val coroutineScope = rememberCoroutineScope() + val snackBarVisible = remember { mutableStateOf(false) } + + val onShowFailureSnackBar: (message: String) -> Unit = { + coroutineScope.launch { + snackBarVisible.value = true + val job = launch { snackBarHostState.showSnackbar(message = it) } + delay(SNACK_BAR_DURATION) + job.cancel() + snackBarVisible.value = false + } + } + LaunchedEffect(key1 = appointmentCheckViewModel.sideEffects) { appointmentCheckViewModel.sideEffects.collect { sideEffect -> when (sideEffect) { @@ -69,6 +97,10 @@ fun AppointmentCheckRoute( sideEffect.show, sideEffect.dialogType ) + + is AppointmentCheckSideEffect.ShowSnackBar -> onShowFailureSnackBar( + context.getString(sideEffect.message) + ) } } } @@ -80,13 +112,16 @@ fun AppointmentCheckRoute( onSelectedDataChange = { selectedData = it }, onBackButtonClick = appointmentCheckViewModel::navigateToGroupDetail, onConfirmButtonClick = { + // TODO: selectedData가 duration 이하인지 체크하는 로직 추가 appointmentCheckViewModel.postTimeTable( groupId, appointmentId, appointmentName, selectedData ) - } + }, + snackBarHostState = snackBarHostState, + snackBarVisible = snackBarVisible ) if (showErrorDialog.first) { @@ -115,7 +150,9 @@ fun AppointmentCheckScreen( availablePeriods: List, onSelectedDataChange: (List) -> Unit = {}, onBackButtonClick: (Long) -> Unit, - onConfirmButtonClick: () -> Unit + onConfirmButtonClick: () -> Unit, + snackBarHostState: SnackbarHostState, + snackBarVisible: MutableState ) { Scaffold( modifier = Modifier @@ -127,6 +164,26 @@ fun AppointmentCheckScreen( isIconVisible = true, onBackButtonClick = { onBackButtonClick(groupId) } ) + }, + snackbarHost = { + AnimatedVisibility( + visible = snackBarVisible.value, + enter = slideInVertically(initialOffsetY = { it }), + exit = slideOutVertically(targetOffsetY = { it }) + ) { + SnackbarHost( + modifier = Modifier.padding(bottom = dimensionResource(id = R.dimen.bottom_padding_snack_bar_non_exist_code)), + hostState = snackBarHostState, + snackbar = { snackBarData -> + NoostakSnackBar( + message = snackBarData.visuals.message, + textStyle = NoostakTheme.typography.c3SemiBold, + textColor = NoostakTheme.colors.red01, + backgroundColor = NoostakTheme.colors.pink + ) + } + ) + } } ) { innerPadding -> Box( @@ -195,7 +252,9 @@ fun PreviewAppointmentConfirmScreen() { ) ), onBackButtonClick = {}, - onConfirmButtonClick = {} + onConfirmButtonClick = {}, + snackBarHostState = SnackbarHostState(), + snackBarVisible = remember { mutableStateOf(true) } ) } } diff --git a/presentation/src/main/java/com/sopt/presentation/appointment/appointmentCheck/AppointmentCheckViewModel.kt b/presentation/src/main/java/com/sopt/presentation/appointment/appointmentCheck/AppointmentCheckViewModel.kt index e0751db6..034bd5b0 100644 --- a/presentation/src/main/java/com/sopt/presentation/appointment/appointmentCheck/AppointmentCheckViewModel.kt +++ b/presentation/src/main/java/com/sopt/presentation/appointment/appointmentCheck/AppointmentCheckViewModel.kt @@ -105,4 +105,6 @@ sealed class AppointmentCheckSideEffect { data class NavigateToGroupDetail(val groupId: Long) : AppointmentCheckSideEffect() data class ShowErrorDialog(val show: Boolean, val dialogType: DialogType) : AppointmentCheckSideEffect() + + data class ShowSnackBar(val message: Int) : AppointmentCheckSideEffect() } diff --git a/presentation/src/main/java/com/sopt/presentation/appointment/navigation/AppointmentNavigation.kt b/presentation/src/main/java/com/sopt/presentation/appointment/navigation/AppointmentNavigation.kt index db8b618b..4e8e9f4e 100644 --- a/presentation/src/main/java/com/sopt/presentation/appointment/navigation/AppointmentNavigation.kt +++ b/presentation/src/main/java/com/sopt/presentation/appointment/navigation/AppointmentNavigation.kt @@ -41,13 +41,15 @@ fun NavController.navigateAppointmentCheck( groupId: Long, appointmentId: Long, appointmentName: String, + duration: Long, navOptions: NavOptions? = null ) { navigate( route = AppointmentCheck( groupId = groupId, appointmentId = appointmentId, - appointmentName = appointmentName + appointmentName = appointmentName, + duration = duration ), navOptions = navOptions ) @@ -82,7 +84,7 @@ fun NavGraphBuilder.appointmentNavGraph( appointmentId = args.appointmentId, appointmentName = args.appointmentName, navigateUp = navHostController::navigateUp, - navigateToAppointmentCheck = { groupId, appointmentId, appointmentName, availablePeriods -> + navigateToAppointmentCheck = { groupId, appointmentId, appointmentName, availablePeriods, duration -> navHostController.currentBackStackEntry?.savedStateHandle?.set( "availablePeriods", availablePeriods @@ -90,7 +92,8 @@ fun NavGraphBuilder.appointmentNavGraph( navHostController.navigateAppointmentCheck( groupId = groupId, appointmentId = appointmentId, - appointmentName = appointmentName + appointmentName = appointmentName, + duration = duration ) }, navigateToAppointmentConfirm = { groupId, appointmentId, optionId, appointmentName, isHost -> @@ -116,6 +119,7 @@ fun NavGraphBuilder.appointmentNavGraph( appointmentId = args.appointmentId, appointmentName = args.appointmentName, availablePeriods = availablePeriods, + duration = args.duration, navigateUp = navHostController::navigateUp, navigateToAppointment = { groupId, appointmentId, appointmentName -> navHostController.navigateAppointment( @@ -156,7 +160,8 @@ data class Appointment( data class AppointmentCheck( val groupId: Long, val appointmentId: Long, - val appointmentName: String + val appointmentName: String, + val duration: Long ) : Route @Serializable diff --git a/presentation/src/main/java/com/sopt/presentation/appointmentCreate/appointmentCreateInfo/AppointmentCreateInfoRoute.kt b/presentation/src/main/java/com/sopt/presentation/appointmentCreate/appointmentCreateInfo/AppointmentCreateInfoRoute.kt index 30a13acd..31876e69 100644 --- a/presentation/src/main/java/com/sopt/presentation/appointmentCreate/appointmentCreateInfo/AppointmentCreateInfoRoute.kt +++ b/presentation/src/main/java/com/sopt/presentation/appointmentCreate/appointmentCreateInfo/AppointmentCreateInfoRoute.kt @@ -304,9 +304,9 @@ fun AppointmentCreateInfoScreen( val time = appointmentDuration.toIntOrNull() ?: 0 onButtonClick(groupId, trimmedName, appointmentCategory, time) }, - isEnabled = appointmentName.isNotEmpty() &&appointmentCategory.isNotBlank() && - appointmentDuration.isNotBlank() && - (appointmentDuration.toIntOrNull()?.let { it in 1..10 } == true), + isEnabled = appointmentName.isNotEmpty() && appointmentCategory.isNotBlank() && + appointmentDuration.isNotBlank() && + (appointmentDuration.toIntOrNull()?.let { it in 1..10 } == true), deactivateColor = NoostakTheme.colors.gray500, activateColor = NoostakTheme.colors.gray900 ) @@ -329,7 +329,7 @@ fun AppointmentCreateInfoScreenPreview() { snackBarVisible = snackBarVisible, showSnackBar = { snackBarVisible.value = true - }, + } ) } } diff --git a/presentation/src/main/java/com/sopt/presentation/calendar/CalendarRoute.kt b/presentation/src/main/java/com/sopt/presentation/calendar/CalendarRoute.kt index da1fba2f..6c69ca33 100644 --- a/presentation/src/main/java/com/sopt/presentation/calendar/CalendarRoute.kt +++ b/presentation/src/main/java/com/sopt/presentation/calendar/CalendarRoute.kt @@ -87,7 +87,7 @@ fun CalendarRoute( val currentYearMonth by remember { derivedStateOf { getYearMonthByPage(pagerState.currentPage) } } var clickDate by remember { mutableStateOf(null) } - var clickAppointmentId by remember { mutableLongStateOf(-1) } + var clickOptionId by remember { mutableLongStateOf(-1) } LaunchedEffect(pagerState) { snapshotFlow { pagerState.currentPage } @@ -118,7 +118,7 @@ fun CalendarRoute( NoostakDialog( dialogType = DialogType.DATA_FAILURE, onClick = { - calendarViewModel.getConfirmedDetail(clickAppointmentId) + calendarViewModel.getConfirmedDetail(clickOptionId) }, onDismissRequest = { calendarViewModel.showDataErrorDialog(false) @@ -158,7 +158,8 @@ fun CalendarRoute( date = date, scheduleList = calendarViewModel.selectedDayAppointments.value.map { CalendarAppointmentEntity( - id = it.id, + appointmentId = it.appointmentId, + optionId = it.optionId, name = it.name, category = it.category, startTime = it.startTime, @@ -169,8 +170,8 @@ fun CalendarRoute( } ), onItemClick = { id -> - clickAppointmentId = id - calendarViewModel.getConfirmedDetail(clickAppointmentId) + clickOptionId = id + calendarViewModel.getConfirmedDetail(clickOptionId) navController.navigate(SCHEDULE_DETAIL) }, onCreateAppointmentBtnClick = { diff --git a/presentation/src/main/java/com/sopt/presentation/calendar/CalendarViewModel.kt b/presentation/src/main/java/com/sopt/presentation/calendar/CalendarViewModel.kt index 4048fc9d..90943ea5 100644 --- a/presentation/src/main/java/com/sopt/presentation/calendar/CalendarViewModel.kt +++ b/presentation/src/main/java/com/sopt/presentation/calendar/CalendarViewModel.kt @@ -66,15 +66,17 @@ class CalendarViewModel @Inject constructor( getGroups() } - fun getConfirmedDetail(appointmentId: Long) { + fun getConfirmedDetail(optionId: Long) { viewModelScope.launch { _getConfirmedDetailState.emit(UiState.Loading) - groupDetailRepository.getConfirmedDetail(appointmentId) - .fold(onSuccess = { _getConfirmedDetailState.emit(UiState.Success(it)) }, + groupDetailRepository.getConfirmedDetail(optionId) + .fold( + onSuccess = { _getConfirmedDetailState.emit(UiState.Success(it)) }, onFailure = { triggerDataErrorDialog() _getConfirmedDetailState.emit(UiState.Failure(it.message.toString())) - }) + } + ) } } @@ -143,7 +145,7 @@ class CalendarViewModel @Inject constructor( LocalDate.of(year, month, dayAppointments.day) .toDateString() to dayAppointments.appointments.map { appointment -> CalendarSchedule( - scrapId = appointment.id, + scrapId = appointment.appointmentId, title = appointment.name, categoryType = appointment.category ) @@ -173,8 +175,8 @@ class CalendarViewModel @Inject constructor( val appointments = _currentMonthAppointments .firstOrNull { it.day == date.dayOfMonth && - currentYearMonth.year == date.year && - currentYearMonth.monthValue == date.monthValue + currentYearMonth.year == date.year && + currentYearMonth.monthValue == date.monthValue }?.appointments ?: emptyList() _selectedDayAppointments.value = appointments diff --git a/presentation/src/main/java/com/sopt/presentation/calendar/component/bottomsheet/ScheduleItem.kt b/presentation/src/main/java/com/sopt/presentation/calendar/component/bottomsheet/ScheduleItem.kt index 9c703ab0..ccb2a50f 100644 --- a/presentation/src/main/java/com/sopt/presentation/calendar/component/bottomsheet/ScheduleItem.kt +++ b/presentation/src/main/java/com/sopt/presentation/calendar/component/bottomsheet/ScheduleItem.kt @@ -31,7 +31,7 @@ fun ScheduleItem( verticalAlignment = Alignment.CenterVertically, modifier = Modifier .fillMaxWidth() - .noRippleClickable { onItemClick(data.id) } + .noRippleClickable { onItemClick(data.optionId) } .background(color = NoostakTheme.colors.gray50, shape = RoundedCornerShape(8.dp)) .padding(8.dp) ) { diff --git a/presentation/src/main/java/com/sopt/presentation/calendar/component/bottomsheet/ScheduleListScreen.kt b/presentation/src/main/java/com/sopt/presentation/calendar/component/bottomsheet/ScheduleListScreen.kt index 8668844f..b17919f1 100644 --- a/presentation/src/main/java/com/sopt/presentation/calendar/component/bottomsheet/ScheduleListScreen.kt +++ b/presentation/src/main/java/com/sopt/presentation/calendar/component/bottomsheet/ScheduleListScreen.kt @@ -65,7 +65,7 @@ fun ScheduleListScreen( ) { itemsIndexed( items = data.scheduleList, - key = { _, item -> item.id } + key = { _, item -> item.appointmentId } ) { index, item -> ScheduleItem(data = item, onItemClick = onItemClick) } @@ -104,7 +104,8 @@ fun ScheduleListScreenPreview() { date = LocalDate.of(2025, 4, 4), scheduleList = listOf( CalendarAppointmentEntity( - id = 1, + appointmentId = 1, + optionId = 1, name = "누스탁 회의dasfdsafsafdafdafsdfsafdsafdsadsdafsadfsdfsdafadafdsafsdafsaf", category = "중요", startTime = "2024-09-07T00:00:00", @@ -113,7 +114,8 @@ fun ScheduleListScreenPreview() { date = "" ), CalendarAppointmentEntity( - id = 2, + appointmentId = 2, + optionId = 2, name = "누스탁 모각작", category = "일정", startTime = "2024-09-07T06:00:00", @@ -122,7 +124,8 @@ fun ScheduleListScreenPreview() { date = "" ), CalendarAppointmentEntity( - id = 3, + appointmentId = 3, + optionId = 3, name = "누스탁 회식", category = "취미", startTime = "2024-09-07T12:00:00",