diff --git a/core/designsystem/src/main/java/com/terning/core/designsystem/extension/FlowExt.kt b/core/designsystem/src/main/java/com/terning/core/designsystem/extension/FlowExt.kt new file mode 100644 index 000000000..981c7b437 --- /dev/null +++ b/core/designsystem/src/main/java/com/terning/core/designsystem/extension/FlowExt.kt @@ -0,0 +1,24 @@ +package com.terning.core.designsystem.extension + +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.SendChannel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.consumeAsFlow +import kotlinx.coroutines.flow.flow + +fun Flow.groupBy(getKey: (T) -> K): Flow>> = flow { + val storage = mutableMapOf>() + try { + collect { t -> + val key = getKey(t) + val channel = storage.getOrPut(key) { + Channel(capacity = Channel.BUFFERED).also { + emit(key to it.consumeAsFlow()) + } + } + channel.send(t) + } + } finally { + storage.values.forEach { it.close() } + } +} \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/terning/core/designsystem/type/AlarmType.kt b/core/designsystem/src/main/java/com/terning/core/designsystem/type/AlarmType.kt new file mode 100644 index 000000000..70106d955 --- /dev/null +++ b/core/designsystem/src/main/java/com/terning/core/designsystem/type/AlarmType.kt @@ -0,0 +1,6 @@ +package com.terning.core.designsystem.type + +enum class AlarmType(val value: String) { + ENABLED("ENABLED"), + DISABLED("DISABLED") +} diff --git a/data/mypage/src/main/java/com/terning/data/mypage/datasource/MyPageDataSource.kt b/data/mypage/src/main/java/com/terning/data/mypage/datasource/MyPageDataSource.kt index 82733b8f2..bbbb4dfef 100644 --- a/data/mypage/src/main/java/com/terning/data/mypage/datasource/MyPageDataSource.kt +++ b/data/mypage/src/main/java/com/terning/data/mypage/datasource/MyPageDataSource.kt @@ -2,6 +2,7 @@ package com.terning.data.mypage.datasource import com.terning.core.network.BaseResponse import com.terning.core.network.NonDataBaseResponse +import com.terning.data.mypage.dto.request.AlarmStatusRequestDto import com.terning.data.mypage.dto.request.MyPageProfileEditRequestDto import com.terning.data.mypage.dto.response.MyPageResponseDto @@ -15,4 +16,8 @@ interface MyPageDataSource { suspend fun editProfile( request: MyPageProfileEditRequestDto ): NonDataBaseResponse -} \ No newline at end of file + + suspend fun updateAlarmState( + request : AlarmStatusRequestDto + ) : NonDataBaseResponse +} diff --git a/data/mypage/src/main/java/com/terning/data/mypage/datasourceimpl/MyPageDataSourceImpl.kt b/data/mypage/src/main/java/com/terning/data/mypage/datasourceimpl/MyPageDataSourceImpl.kt index a8c1fe64b..dbf576030 100644 --- a/data/mypage/src/main/java/com/terning/data/mypage/datasourceimpl/MyPageDataSourceImpl.kt +++ b/data/mypage/src/main/java/com/terning/data/mypage/datasourceimpl/MyPageDataSourceImpl.kt @@ -3,6 +3,7 @@ package com.terning.data.mypage.datasourceimpl import com.terning.core.network.BaseResponse import com.terning.core.network.NonDataBaseResponse import com.terning.data.mypage.datasource.MyPageDataSource +import com.terning.data.mypage.dto.request.AlarmStatusRequestDto import com.terning.data.mypage.dto.request.MyPageProfileEditRequestDto import com.terning.data.mypage.dto.response.MyPageResponseDto import com.terning.data.mypage.service.MyPageService @@ -20,4 +21,7 @@ class MyPageDataSourceImpl @Inject constructor( override suspend fun editProfile( request: MyPageProfileEditRequestDto ): NonDataBaseResponse = myPageService.editProfile(request) -} \ No newline at end of file + + override suspend fun updateAlarmState(request: AlarmStatusRequestDto): NonDataBaseResponse = + myPageService.patchAlarmStatus(request) +} diff --git a/data/mypage/src/main/java/com/terning/data/mypage/dto/request/AlarmStatusRequestDto.kt b/data/mypage/src/main/java/com/terning/data/mypage/dto/request/AlarmStatusRequestDto.kt new file mode 100644 index 000000000..4df436809 --- /dev/null +++ b/data/mypage/src/main/java/com/terning/data/mypage/dto/request/AlarmStatusRequestDto.kt @@ -0,0 +1,10 @@ +package com.terning.data.mypage.dto.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class AlarmStatusRequestDto( + @SerialName("newStatus") + val newStatus: String +) diff --git a/data/mypage/src/main/java/com/terning/data/mypage/dto/response/MyPageResponseDto.kt b/data/mypage/src/main/java/com/terning/data/mypage/dto/response/MyPageResponseDto.kt index 0cca39b7f..c99bbeffc 100644 --- a/data/mypage/src/main/java/com/terning/data/mypage/dto/response/MyPageResponseDto.kt +++ b/data/mypage/src/main/java/com/terning/data/mypage/dto/response/MyPageResponseDto.kt @@ -10,5 +10,7 @@ data class MyPageResponseDto( @SerialName("profileImage") val profileImage: String, @SerialName("authType") - val authType: String -) \ No newline at end of file + val authType: String, + @SerialName("pushStatus") + val pushStatus: String +) diff --git a/data/mypage/src/main/java/com/terning/data/mypage/mapper/AlarmStatusMapper.kt b/data/mypage/src/main/java/com/terning/data/mypage/mapper/AlarmStatusMapper.kt new file mode 100644 index 000000000..7f81d4dc2 --- /dev/null +++ b/data/mypage/src/main/java/com/terning/data/mypage/mapper/AlarmStatusMapper.kt @@ -0,0 +1,9 @@ +package com.terning.data.mypage.mapper + +import com.terning.data.mypage.dto.request.AlarmStatusRequestDto +import com.terning.domain.mypage.entity.AlarmStatus + +fun AlarmStatus.toAlarmStatusRequestDto(): AlarmStatusRequestDto = + AlarmStatusRequestDto( + newStatus = newStatus + ) diff --git a/data/mypage/src/main/java/com/terning/data/mypage/mapper/MyPageMapper.kt b/data/mypage/src/main/java/com/terning/data/mypage/mapper/MyPageMapper.kt index 3a46f7104..9868c8211 100644 --- a/data/mypage/src/main/java/com/terning/data/mypage/mapper/MyPageMapper.kt +++ b/data/mypage/src/main/java/com/terning/data/mypage/mapper/MyPageMapper.kt @@ -7,5 +7,6 @@ fun MyPageResponseDto.toMyPageProfile() = MyPageProfile( name = name, profileImage = profileImage, - authType = authType + authType = authType, + alarmStatus = pushStatus ) diff --git a/data/mypage/src/main/java/com/terning/data/mypage/repositoryimpl/MyPageRepositoryImpl.kt b/data/mypage/src/main/java/com/terning/data/mypage/repositoryimpl/MyPageRepositoryImpl.kt index dbff41328..ee8eae27c 100644 --- a/data/mypage/src/main/java/com/terning/data/mypage/repositoryimpl/MyPageRepositoryImpl.kt +++ b/data/mypage/src/main/java/com/terning/data/mypage/repositoryimpl/MyPageRepositoryImpl.kt @@ -1,8 +1,10 @@ package com.terning.data.mypage.repositoryimpl import com.terning.data.mypage.datasource.MyPageDataSource +import com.terning.data.mypage.mapper.toAlarmStatusRequestDto import com.terning.data.mypage.mapper.toMyPageProfile import com.terning.data.mypage.mapper.toMyPageProfileEditRequestDto +import com.terning.domain.mypage.entity.AlarmStatus import com.terning.domain.mypage.entity.MyPageProfile import com.terning.domain.mypage.entity.MyPageProfileEdit import com.terning.domain.mypage.repository.MyPageRepository @@ -34,4 +36,9 @@ class MyPageRepositoryImpl @Inject constructor( request.toMyPageProfileEditRequestDto() ) } -} \ No newline at end of file + + override suspend fun updateAlarmState(request: AlarmStatus): Result = + runCatching { + myPageDataSource.updateAlarmState(request.toAlarmStatusRequestDto()) + } +} diff --git a/data/mypage/src/main/java/com/terning/data/mypage/service/MyPageService.kt b/data/mypage/src/main/java/com/terning/data/mypage/service/MyPageService.kt index ece71a44c..443174518 100644 --- a/data/mypage/src/main/java/com/terning/data/mypage/service/MyPageService.kt +++ b/data/mypage/src/main/java/com/terning/data/mypage/service/MyPageService.kt @@ -2,6 +2,7 @@ package com.terning.data.mypage.service import com.terning.core.network.BaseResponse import com.terning.core.network.NonDataBaseResponse +import com.terning.data.mypage.dto.request.AlarmStatusRequestDto import com.terning.data.mypage.dto.request.MyPageProfileEditRequestDto import com.terning.data.mypage.dto.response.MyPageResponseDto import retrofit2.http.Body @@ -24,4 +25,9 @@ interface MyPageService { suspend fun editProfile( @Body body: MyPageProfileEditRequestDto ): NonDataBaseResponse + + @PATCH("api/v1/push-status") + suspend fun patchAlarmStatus( + @Body body: AlarmStatusRequestDto + ): NonDataBaseResponse } \ No newline at end of file diff --git a/domain/mypage/src/main/java/com/terning/domain/mypage/entity/AlarmStatus.kt b/domain/mypage/src/main/java/com/terning/domain/mypage/entity/AlarmStatus.kt new file mode 100644 index 000000000..b4e414ebd --- /dev/null +++ b/domain/mypage/src/main/java/com/terning/domain/mypage/entity/AlarmStatus.kt @@ -0,0 +1,5 @@ +package com.terning.domain.mypage.entity + +data class AlarmStatus( + val newStatus: String +) diff --git a/domain/mypage/src/main/java/com/terning/domain/mypage/entity/MyPageProfile.kt b/domain/mypage/src/main/java/com/terning/domain/mypage/entity/MyPageProfile.kt index afff9a0fb..85ed630ec 100644 --- a/domain/mypage/src/main/java/com/terning/domain/mypage/entity/MyPageProfile.kt +++ b/domain/mypage/src/main/java/com/terning/domain/mypage/entity/MyPageProfile.kt @@ -3,5 +3,6 @@ package com.terning.domain.mypage.entity data class MyPageProfile( val name: String, val profileImage: String, - val authType: String -) \ No newline at end of file + val authType: String, + val alarmStatus: String +) diff --git a/domain/mypage/src/main/java/com/terning/domain/mypage/repository/MyPageRepository.kt b/domain/mypage/src/main/java/com/terning/domain/mypage/repository/MyPageRepository.kt index e9478f285..a7b5657aa 100644 --- a/domain/mypage/src/main/java/com/terning/domain/mypage/repository/MyPageRepository.kt +++ b/domain/mypage/src/main/java/com/terning/domain/mypage/repository/MyPageRepository.kt @@ -1,5 +1,6 @@ package com.terning.domain.mypage.repository +import com.terning.domain.mypage.entity.AlarmStatus import com.terning.domain.mypage.entity.MyPageProfile import com.terning.domain.mypage.entity.MyPageProfileEdit @@ -13,4 +14,8 @@ interface MyPageRepository { suspend fun editProfile( request: MyPageProfileEdit ): Result -} \ No newline at end of file + + suspend fun updateAlarmState( + request: AlarmStatus + ): Result +} diff --git a/feature/home/src/main/java/com/terning/feature/home/HomeRoute.kt b/feature/home/src/main/java/com/terning/feature/home/HomeRoute.kt index fe9a6b202..2f3472383 100644 --- a/feature/home/src/main/java/com/terning/feature/home/HomeRoute.kt +++ b/feature/home/src/main/java/com/terning/feature/home/HomeRoute.kt @@ -105,6 +105,10 @@ fun HomeRoute( viewModel.updatePermissionRequested(true) } } + else { + val isAlarmAvailable = viewModel.getAlarmAvailability() + viewModel.updateAlarmAvailability(isAlarmAvailable) + } } var hasHandledInternDeeplink by rememberSaveable { mutableStateOf(false) } diff --git a/feature/home/src/main/java/com/terning/feature/home/HomeViewModel.kt b/feature/home/src/main/java/com/terning/feature/home/HomeViewModel.kt index 6b1fa6375..00fbe574d 100644 --- a/feature/home/src/main/java/com/terning/feature/home/HomeViewModel.kt +++ b/feature/home/src/main/java/com/terning/feature/home/HomeViewModel.kt @@ -6,12 +6,15 @@ import androidx.paging.PagingData import androidx.paging.cachedIn import androidx.paging.map import com.terning.core.designsystem.state.UiState +import com.terning.core.designsystem.type.AlarmType.DISABLED +import com.terning.core.designsystem.type.AlarmType.ENABLED import com.terning.core.designsystem.type.SortBy import com.terning.domain.home.entity.ChangeFilteringRequestModel import com.terning.domain.home.entity.FcmToken import com.terning.domain.home.entity.HomeRecommendIntern import com.terning.domain.home.entity.HomeRecommendedIntern import com.terning.domain.home.repository.HomeRepository +import com.terning.domain.mypage.entity.AlarmStatus import com.terning.domain.mypage.repository.MyPageRepository import com.terning.domain.user.repository.UserRepository import dagger.hilt.android.lifecycle.HiltViewModel @@ -220,6 +223,11 @@ class HomeViewModel @Inject constructor( fun updateAlarmAvailability(availability: Boolean) { userRepository.setAlarmAvailable(availability) + + viewModelScope.launch { + if (availability) myPageRepository.updateAlarmState(AlarmStatus(ENABLED.value)) + else myPageRepository.updateAlarmState(AlarmStatus(DISABLED.value)) + } } fun updatePermissionRequested(requested: Boolean) { @@ -244,4 +252,6 @@ class HomeViewModel @Inject constructor( ).onFailure(Timber::e) } } + + fun getAlarmAvailability(): Boolean = userRepository.getAlarmAvailable() } \ No newline at end of file diff --git a/feature/mypage/src/main/java/com/terning/feature/mypage/mypage/MyPageRoute.kt b/feature/mypage/src/main/java/com/terning/feature/mypage/mypage/MyPageRoute.kt index c80f7b245..14a811b6e 100644 --- a/feature/mypage/src/main/java/com/terning/feature/mypage/mypage/MyPageRoute.kt +++ b/feature/mypage/src/main/java/com/terning/feature/mypage/mypage/MyPageRoute.kt @@ -94,8 +94,12 @@ fun MyPageRoute( rememberPermissionState(permission = notificationPermission) var isChecked by remember { mutableStateOf( - if (!permissionState.status.isGranted) false - else viewModel.getAlarmAvailability() + if (!permissionState.status.isGranted) { + viewModel.updateAlarmAvailability(false) + false + } else { + viewModel.getAlarmAvailability() + } ) } val notificationSettingsLauncher = @@ -106,17 +110,15 @@ fun MyPageRoute( viewModel.updateAlarmAvailability(isGranted) } - LaunchedEffect(Unit) { + DisposableEffect(lifecycleOwner) { systemUiController.setStatusBarColor(color = Back) - } - DisposableEffect(lifecycleOwner) { onDispose { systemUiController.setStatusBarColor(color = White) } } - LaunchedEffect(key1 = true) { + LaunchedEffect(Unit) { viewModel.getProfile() } diff --git a/feature/mypage/src/main/java/com/terning/feature/mypage/mypage/MyPageState.kt b/feature/mypage/src/main/java/com/terning/feature/mypage/mypage/MyPageState.kt index ea480f8bc..56e49fc44 100644 --- a/feature/mypage/src/main/java/com/terning/feature/mypage/mypage/MyPageState.kt +++ b/feature/mypage/src/main/java/com/terning/feature/mypage/mypage/MyPageState.kt @@ -1,6 +1,7 @@ package com.terning.feature.mypage.mypage import com.terning.core.designsystem.state.UiState +import com.terning.core.designsystem.type.AlarmType.DISABLED data class MyPageState( val isGetSuccess: UiState = UiState.Loading, @@ -13,5 +14,6 @@ data class MyPageState( val showPersonal: Boolean = false, val showLogoutBottomSheet: Boolean = false, val showQuitBottomSheet: Boolean = false, - val showAlarmDialog: Boolean = false -) \ No newline at end of file + val showAlarmDialog: Boolean = false, + val alarmStatus: String = DISABLED.value, +) diff --git a/feature/mypage/src/main/java/com/terning/feature/mypage/mypage/MyPageViewModel.kt b/feature/mypage/src/main/java/com/terning/feature/mypage/mypage/MyPageViewModel.kt index 598a644c2..5ec56d298 100644 --- a/feature/mypage/src/main/java/com/terning/feature/mypage/mypage/MyPageViewModel.kt +++ b/feature/mypage/src/main/java/com/terning/feature/mypage/mypage/MyPageViewModel.kt @@ -3,21 +3,31 @@ package com.terning.feature.mypage.mypage import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.kakao.sdk.user.UserApiClient +import com.terning.core.designsystem.extension.groupBy import com.terning.core.designsystem.state.UiState +import com.terning.core.designsystem.type.AlarmType.DISABLED +import com.terning.core.designsystem.type.AlarmType.ENABLED +import com.terning.domain.mypage.entity.AlarmStatus import com.terning.domain.mypage.repository.MyPageRepository import com.terning.domain.user.repository.UserRepository +import com.terning.feature.mypage.mypage.model.AlarmInfo import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.flatMapMerge import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject import com.terning.core.designsystem.R as DesignSystemR +@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) @HiltViewModel class MyPageViewModel @Inject constructor( private val myPageRepository: MyPageRepository, @@ -30,6 +40,37 @@ class MyPageViewModel @Inject constructor( private val _sideEffects = MutableSharedFlow() val sideEffects: SharedFlow get() = _sideEffects.asSharedFlow() + private val debounceFlow = MutableSharedFlow() + + private val lastSuccessfulAlarmStatus = mutableMapOf() + + init { + handleDebouncedAlarm() + } + + private fun handleDebouncedAlarm() { + viewModelScope.launch { + debounceFlow + .groupBy { it.id } + .flatMapMerge { (_, flow) -> flow.debounce(DEBOUNCE_DURATION) } + .collect { info -> + myPageRepository.updateAlarmState( + AlarmStatus(if (info.isAlarmAvailable) ENABLED.value else DISABLED.value) + ).onSuccess { + lastSuccessfulAlarmStatus[info.id] = info.isAlarmAvailable + }.onFailure { + val previous = lastSuccessfulAlarmStatus[info.id] ?: !info.isAlarmAvailable + _state.update { currentState -> + currentState.copy(alarmStatus = if (previous) ENABLED.value else DISABLED.value) + } + userRepository.setAlarmAvailable(previous) + + _sideEffects.emit(MyPageSideEffect.ShowToast(DesignSystemR.string.server_failure)) + } + } + } + } + fun logoutKakao() { UserApiClient.instance.logout { error -> if (error == null) { @@ -80,15 +121,19 @@ class MyPageViewModel @Inject constructor( viewModelScope.launch { myPageRepository.getProfile() .onSuccess { response -> - _state.value = _state.value.copy( - isGetSuccess = UiState.Success(true), - name = response.name, - profileImage = response.profileImage, - authType = response.authType - ) + _state.update { currentState -> + currentState.copy( + isGetSuccess = UiState.Success(true), + name = response.name, + profileImage = response.profileImage, + authType = response.authType, + alarmStatus = response.alarmStatus + ) + } }.onFailure { _sideEffects.emit(MyPageSideEffect.ShowToast(DesignSystemR.string.server_failure)) - _state.value = _state.value.copy(isGetSuccess = UiState.Failure(it.toString())) + _state.value = + _state.value.copy(isGetSuccess = UiState.Failure(it.toString())) } } } @@ -133,7 +178,15 @@ class MyPageViewModel @Inject constructor( viewModelScope.launch { _sideEffects.emit(MyPageSideEffect.NavigateToProfileEdit) } fun updateAlarmAvailability(availability: Boolean) { + _state.update { currentState -> + currentState.copy(alarmStatus = if (availability) ENABLED.value else DISABLED.value) + } + userRepository.setAlarmAvailable(availability) + + viewModelScope.launch { + debounceFlow.emit(AlarmInfo(id = DEBOUNCE_KEY, isAlarmAvailable = availability)) + } } fun getAlarmAvailability(): Boolean = userRepository.getAlarmAvailable() @@ -146,4 +199,8 @@ class MyPageViewModel @Inject constructor( } } -} \ No newline at end of file + companion object { + private const val DEBOUNCE_DURATION = 300L + private const val DEBOUNCE_KEY = "NOTIFICATION" + } +} diff --git a/feature/mypage/src/main/java/com/terning/feature/mypage/mypage/model/AlarmInfo.kt b/feature/mypage/src/main/java/com/terning/feature/mypage/mypage/model/AlarmInfo.kt new file mode 100644 index 000000000..e9a7b8a80 --- /dev/null +++ b/feature/mypage/src/main/java/com/terning/feature/mypage/mypage/model/AlarmInfo.kt @@ -0,0 +1,6 @@ +package com.terning.feature.mypage.mypage.model + +internal data class AlarmInfo( + val id: String, + val isAlarmAvailable: Boolean +) \ No newline at end of file