diff --git a/data/repository/src/main/java/co/kr/data/repository/ConnectRepositoryImpl.kt b/data/repository/src/main/java/co/kr/data/repository/ConnectRepositoryImpl.kt index 474a11be..029c1ec4 100644 --- a/data/repository/src/main/java/co/kr/data/repository/ConnectRepositoryImpl.kt +++ b/data/repository/src/main/java/co/kr/data/repository/ConnectRepositoryImpl.kt @@ -67,7 +67,7 @@ internal class ConnectRepositoryImpl @Inject constructor( return response.toDomain() } - override suspend fun getHomeDialogHiddenDate(): Flow = + override suspend fun getExplicitDeniedConnectDate(): Flow = connectLocalDataSource.explicitDeniedConnectDate.map { dateString -> dateString?.let { runCatching { dateFormatter.parseDateTime(it) } @@ -75,7 +75,7 @@ internal class ConnectRepositoryImpl @Inject constructor( } } - override suspend fun updateHomeDialogHiddenDate(date: LocalDateTime) { + override suspend fun updateExplicitDeniedConnectDate(date: LocalDateTime) { val formattedDate = dateFormatter.format(date) connectLocalDataSource.updateExplicitDeniedConnectDate(formattedDate) } diff --git a/domain/src/main/java/co/kr/tnt/domain/repository/ConnectRepository.kt b/domain/src/main/java/co/kr/tnt/domain/repository/ConnectRepository.kt index aeeccfa5..ea742cae 100644 --- a/domain/src/main/java/co/kr/tnt/domain/repository/ConnectRepository.kt +++ b/domain/src/main/java/co/kr/tnt/domain/repository/ConnectRepository.kt @@ -25,7 +25,7 @@ interface ConnectRepository { traineeId: String, ): ConnectedResult - suspend fun getHomeDialogHiddenDate(): Flow + suspend fun getExplicitDeniedConnectDate(): Flow - suspend fun updateHomeDialogHiddenDate(date: LocalDateTime) + suspend fun updateExplicitDeniedConnectDate(date: LocalDateTime) } diff --git a/feature/trainee/home/src/main/java/co/kr/tnt/trainee/home/TraineeHomeViewModel.kt b/feature/trainee/home/src/main/java/co/kr/tnt/trainee/home/TraineeHomeViewModel.kt index 424a2f84..91ee387b 100644 --- a/feature/trainee/home/src/main/java/co/kr/tnt/trainee/home/TraineeHomeViewModel.kt +++ b/feature/trainee/home/src/main/java/co/kr/tnt/trainee/home/TraineeHomeViewModel.kt @@ -143,7 +143,7 @@ internal class TraineeHomeViewModel @Inject constructor( private fun updateCurrentDateTime() { val currentDateTime = LocalDateTime.now() viewModelScope.launch { - connectRepository.updateHomeDialogHiddenDate(currentDateTime) + connectRepository.updateExplicitDeniedConnectDate(currentDateTime) } } @@ -169,7 +169,7 @@ internal class TraineeHomeViewModel @Inject constructor( sendEffect(TraineeHomeEffect.ShowToast("서버 요청에 실패했어요.")) } - val lastHiddenDate = connectRepository.getHomeDialogHiddenDate().firstOrNull() + val lastHiddenDate = connectRepository.getExplicitDeniedConnectDate().firstOrNull() val isHidden = lastHiddenDate != null && Duration.between(lastHiddenDate, currentDateTime).toHours() < DIALOG_HIDE_DURATION_HOURS diff --git a/feature/trainer/home/src/main/java/co/kr/tnt/trainer/home/TrainerHomeContract.kt b/feature/trainer/home/src/main/java/co/kr/tnt/trainer/home/TrainerHomeContract.kt index bbb100cd..4a75d6cb 100644 --- a/feature/trainer/home/src/main/java/co/kr/tnt/trainer/home/TrainerHomeContract.kt +++ b/feature/trainer/home/src/main/java/co/kr/tnt/trainer/home/TrainerHomeContract.kt @@ -12,7 +12,15 @@ internal class TrainerHomeContract { val selectedDay: LocalDate = LocalDate.now(), val dailyPtSessionCount: Map = mapOf(), val selectedDayPtSessions: List? = null, - ) : UiState + val dialogState: DialogState = DialogState.NONE, + val isDialogHiddenForThreeDays: Boolean = false, + ) : UiState { + enum class DialogState { + NONE, + HOME_CONNECT, + ADD_PT_CONNECT, + } + } sealed interface TrainerHomeUiEvent : UiEvent { data object OnScreen : TrainerHomeUiEvent @@ -21,11 +29,15 @@ internal class TrainerHomeContract { data class OnClickDay(val day: LocalDate) : TrainerHomeUiEvent data object OnClickAddPtSession : TrainerHomeUiEvent data class OnClickPtSessionComplete(val ptSession: PtSession) : TrainerHomeUiEvent + data object OnConfirmConnectDialog : TrainerHomeUiEvent + data object OnChangeHideDialogOption : TrainerHomeUiEvent + data object OnDismissDialog : TrainerHomeUiEvent } sealed interface TrainerHomeSideEffect : UiSideEffect { data object NavigateToNotification : TrainerHomeSideEffect data object NavigateToAddPtSession : TrainerHomeSideEffect + data object NavigateToInvite : TrainerHomeSideEffect data class ShowToast(val message: String) : TrainerHomeSideEffect } } diff --git a/feature/trainer/home/src/main/java/co/kr/tnt/trainer/home/TrainerHomeScreen.kt b/feature/trainer/home/src/main/java/co/kr/tnt/trainer/home/TrainerHomeScreen.kt index 3a41e324..2bb50cda 100644 --- a/feature/trainer/home/src/main/java/co/kr/tnt/trainer/home/TrainerHomeScreen.kt +++ b/feature/trainer/home/src/main/java/co/kr/tnt/trainer/home/TrainerHomeScreen.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight @@ -38,6 +39,7 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import co.kr.tnt.core.designsystem.R +import co.kr.tnt.designsystem.component.TnTPopupDialog import co.kr.tnt.designsystem.component.button.TnTFabButton import co.kr.tnt.designsystem.component.calendar.TnTIndicatorMonthCalendar import co.kr.tnt.designsystem.component.calendar.model.DayIndicatorState @@ -51,6 +53,7 @@ import co.kr.tnt.domain.utils.DateFormatter import co.kr.tnt.trainer.home.TrainerHomeContract.TrainerHomeSideEffect import co.kr.tnt.trainer.home.TrainerHomeContract.TrainerHomeUiEvent import co.kr.tnt.trainer.home.TrainerHomeContract.TrainerHomeUiState +import co.kr.tnt.ui.component.TnTCheckToggleDialog import co.kr.tnt.ui.component.TnTHomeTopBar import coil.compose.rememberAsyncImagePainter import coil.request.ImageRequest @@ -60,6 +63,7 @@ import kotlinx.coroutines.launch import java.time.DayOfWeek import java.time.LocalDate import java.time.YearMonth +import co.kr.tnt.core.ui.R as coreR @Composable internal fun TrainerHomeRoute( @@ -67,6 +71,7 @@ internal fun TrainerHomeRoute( padding: PaddingValues, navigateToNotification: () -> Unit, navigateToAddPtSession: () -> Unit, + navigateToInvite: (Boolean) -> Unit, ) { val toast = LocalSnackbar.current val state by viewModel.uiState.collectAsStateWithLifecycle() @@ -86,11 +91,42 @@ internal fun TrainerHomeRoute( when (effect) { TrainerHomeSideEffect.NavigateToNotification -> navigateToNotification() TrainerHomeSideEffect.NavigateToAddPtSession -> navigateToAddPtSession() + TrainerHomeSideEffect.NavigateToInvite -> navigateToInvite(false) is TrainerHomeSideEffect.ShowToast -> toast.show(effect.message) } } } + when (state.dialogState) { + TrainerHomeUiState.DialogState.NONE -> Unit + TrainerHomeUiState.DialogState.HOME_CONNECT -> { + TnTCheckToggleDialog( + title = "회원을 연결해 주세요", + content = "연결하지 않을 경우 수업을 추가할 수 없어요\n초대 코드를 복사해 연결해주시겠어요?", + isChecked = state.isDialogHiddenForThreeDays, + checkToggleText = stringResource(coreR.string.do_not_see_for_three_days), + leftButtonText = stringResource(coreR.string.next_time), + rightButtonText = stringResource(coreR.string.connect), + onLeftButtonClick = { viewModel.setEvent(TrainerHomeUiEvent.OnDismissDialog) }, + onRightButtonClick = { viewModel.setEvent(TrainerHomeUiEvent.OnConfirmConnectDialog) }, + onCheckClick = { viewModel.setEvent(TrainerHomeUiEvent.OnChangeHideDialogOption) }, + onDismiss = { viewModel.setEvent(TrainerHomeUiEvent.OnDismissDialog) }, + ) + } + + TrainerHomeUiState.DialogState.ADD_PT_CONNECT -> { + TnTPopupDialog( + title = "회원을 연결해 주세요", + content = "연결하지 않을 경우 수업을 추가할 수 없어요\n초대 코드를 복사해 연결해주시겠어요?", + leftButtonText = stringResource(coreR.string.next_time), + rightButtonText = stringResource(coreR.string.connect), + onLeftButtonClick = { viewModel.setEvent(TrainerHomeUiEvent.OnDismissDialog) }, + onRightButtonClick = { viewModel.setEvent(TrainerHomeUiEvent.OnConfirmConnectDialog) }, + onDismiss = { viewModel.setEvent(TrainerHomeUiEvent.OnDismissDialog) }, + ) + } + } + // TODO 홈 화면 진입 시마다 데이터 조회 재고 필요 LaunchedEffect(true) { viewModel.setEvent(TrainerHomeUiEvent.OnScreen) } } diff --git a/feature/trainer/home/src/main/java/co/kr/tnt/trainer/home/TrainerHomeViewModel.kt b/feature/trainer/home/src/main/java/co/kr/tnt/trainer/home/TrainerHomeViewModel.kt index 79f25a14..66986922 100644 --- a/feature/trainer/home/src/main/java/co/kr/tnt/trainer/home/TrainerHomeViewModel.kt +++ b/feature/trainer/home/src/main/java/co/kr/tnt/trainer/home/TrainerHomeViewModel.kt @@ -3,25 +3,33 @@ package co.kr.tnt.trainer.home import androidx.lifecycle.viewModelScope import co.kr.tnt.domain.model.PtSession import co.kr.tnt.domain.model.trainer.TrainerDailyPtSessionCount +import co.kr.tnt.domain.repository.ConnectRepository import co.kr.tnt.domain.repository.TrainerRepository import co.kr.tnt.trainer.home.TrainerHomeContract.TrainerHomeSideEffect import co.kr.tnt.trainer.home.TrainerHomeContract.TrainerHomeUiEvent import co.kr.tnt.trainer.home.TrainerHomeContract.TrainerHomeUiState +import co.kr.tnt.trainer.home.TrainerHomeContract.TrainerHomeUiState.DialogState import co.kr.tnt.ui.base.BaseViewModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch import kotlinx.coroutines.supervisorScope +import java.time.Duration import java.time.LocalDate +import java.time.LocalDateTime import java.time.YearMonth import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentMap import javax.inject.Inject +const val DIALOG_HIDE_DURATION_HOURS = 72 + @HiltViewModel internal class TrainerHomeViewModel @Inject constructor( private val trainerRepository: TrainerRepository, + private val connectRepository: ConnectRepository, ) : BaseViewModel(TrainerHomeUiState()) { private val cachedMonthlyPtSessionCounts: ConcurrentMap> = @@ -38,8 +46,12 @@ internal class TrainerHomeViewModel @Inject constructor( TrainerHomeUiEvent.OnClickNotification -> sendEffect(TrainerHomeSideEffect.NavigateToNotification) is TrainerHomeUiEvent.OnChangeVisibleMonth -> handleChangeVisibleMonth(event.yearMonth) is TrainerHomeUiEvent.OnClickDay -> selectDay(event.day) - TrainerHomeUiEvent.OnClickAddPtSession -> sendEffect(TrainerHomeSideEffect.NavigateToAddPtSession) + TrainerHomeUiEvent.OnClickAddPtSession -> showConnectDialog(false) + is TrainerHomeUiEvent.OnClickPtSessionComplete -> completePtSession(event.ptSession) + TrainerHomeUiEvent.OnChangeHideDialogOption -> toggleDialogHiddenState() + TrainerHomeUiEvent.OnConfirmConnectDialog -> handleDialogConfirm() + TrainerHomeUiEvent.OnDismissDialog -> dismissDialog() } } @@ -130,5 +142,72 @@ internal class TrainerHomeViewModel @Inject constructor( cachedMonthlyPtSessionCounts.clear() cachedDailyPtSession.clear() selectDay(currentState.selectedDay) + showConnectDialog(true) + } + + private fun showConnectDialog(triggeredByHome: Boolean) { + val currentDateTime = LocalDateTime.now() + + viewModelScope.launch { + runCatching { + trainerRepository.getActiveMembers() + }.onSuccess { result -> + if (result.isNotEmpty()) { + updateState { copy(dialogState = DialogState.NONE) } + if (triggeredByHome.not()) { + sendEffect(TrainerHomeSideEffect.NavigateToAddPtSession) + } + return@launch + } + } + + val lastHiddenDate = connectRepository.getExplicitDeniedConnectDate().firstOrNull() + val isHidden = lastHiddenDate != null && + Duration.between(lastHiddenDate, currentDateTime).toHours() < DIALOG_HIDE_DURATION_HOURS + + if (isHidden.not() && triggeredByHome) { + updateState { copy(dialogState = DialogState.HOME_CONNECT) } + } else if (triggeredByHome.not()) { + updateState { copy(dialogState = DialogState.ADD_PT_CONNECT) } + } + } + } + + private fun toggleDialogHiddenState() { + updateState { copy(isDialogHiddenForThreeDays = !isDialogHiddenForThreeDays) } + } + + private fun handleDialogConfirm() { + if (currentState.isDialogHiddenForThreeDays) { + updateCurrentDateTime() + } + val effect = when (currentState.dialogState) { + DialogState.HOME_CONNECT -> TrainerHomeSideEffect.NavigateToInvite + DialogState.ADD_PT_CONNECT -> TrainerHomeSideEffect.NavigateToInvite + else -> return + } + updateState { + copy(dialogState = DialogState.NONE, isDialogHiddenForThreeDays = false) + } + sendEffect(effect) + } + + private fun updateCurrentDateTime() { + val currentDateTime = LocalDateTime.now() + viewModelScope.launch { + connectRepository.updateExplicitDeniedConnectDate(currentDateTime) + } + } + + private fun dismissDialog() { + if (currentState.isDialogHiddenForThreeDays) { + updateCurrentDateTime() + } + updateState { + copy( + dialogState = DialogState.NONE, + isDialogHiddenForThreeDays = false, + ) + } } } diff --git a/feature/trainer/home/src/main/java/co/kr/tnt/trainer/home/navigation/TrainerHomeNavigation.kt b/feature/trainer/home/src/main/java/co/kr/tnt/trainer/home/navigation/TrainerHomeNavigation.kt index 03aa564f..0dc71dd2 100644 --- a/feature/trainer/home/src/main/java/co/kr/tnt/trainer/home/navigation/TrainerHomeNavigation.kt +++ b/feature/trainer/home/src/main/java/co/kr/tnt/trainer/home/navigation/TrainerHomeNavigation.kt @@ -27,6 +27,7 @@ fun NavGraphBuilder.trainerHomeNavGraph( padding: PaddingValues, navigateToNotification: () -> Unit, navigateToAddPtSession: () -> Unit, + navigateToInvite: (Boolean) -> Unit, homeDestination: NavGraphBuilder.() -> Unit = { }, ) { navigation(startDestination = Route.TrainerHome) { @@ -35,6 +36,7 @@ fun NavGraphBuilder.trainerHomeNavGraph( padding = padding, navigateToNotification = navigateToNotification, navigateToAddPtSession = navigateToAddPtSession, + navigateToInvite = navigateToInvite, ) } homeDestination() diff --git a/feature/trainer/main/src/main/java/co/kr/tnt/trainer/main/TrainerMainScreen.kt b/feature/trainer/main/src/main/java/co/kr/tnt/trainer/main/TrainerMainScreen.kt index a5a272ce..e9ce587f 100644 --- a/feature/trainer/main/src/main/java/co/kr/tnt/trainer/main/TrainerMainScreen.kt +++ b/feature/trainer/main/src/main/java/co/kr/tnt/trainer/main/TrainerMainScreen.kt @@ -70,6 +70,7 @@ private fun TrainerMainScreen( padding = innerPadding, navigateToNotification = navController::navigateToTrainerNotification, navigateToAddPtSession = navController::navigateToAddPtSession, + navigateToInvite = navigateToInvite, ) { trainerNotification( navigateToPrevious = navController::safePopBackStack,