-
Notifications
You must be signed in to change notification settings - Fork 1
[FEAT/#396] 푸시알림 상태 변경 / 서버통신 구현 #398
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 16 commits
278ca75
b9d7155
df03763
7bfd4c5
0e09c28
fce4cfb
e95fb65
ea565ff
f641f5a
5eb3c08
0310185
3332ac3
defb28c
97c5c7a
c134828
8a86ca7
ab37251
d6d9275
fa9dde3
8bfb7aa
a42ed2a
93e4e37
b81f23f
05886e7
da0c921
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| 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 <T, K> Flow<T>.groupBy(getKey: (T) -> K): Flow<Pair<K, Flow<T>>> = flow { | ||
| val storage = mutableMapOf<K, SendChannel<T>>() | ||
|
|
||
| collect { t -> | ||
| val key = getKey(t) | ||
| val channel = storage.getOrPut(key) { | ||
| Channel<T>(capacity = Channel.BUFFERED).also { | ||
| emit(key to it.consumeAsFlow()) | ||
| } | ||
| } | ||
| channel.send(t) | ||
| } | ||
|
|
||
| storage.values.forEach { it.close() } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| package com.terning.core.designsystem.type | ||
|
|
||
| enum class AlarmType(val value: String) { | ||
| ENABLED("ENABLED"), | ||
| DISABLED("DISABLED") | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| package com.terning.domain.mypage.entity | ||
|
|
||
| data class AlarmStatus( | ||
| val newStatus: String | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,5 +3,6 @@ package com.terning.domain.mypage.entity | |
| data class MyPageProfile( | ||
| val name: String, | ||
| val profileImage: String, | ||
| val authType: String | ||
| ) | ||
| val authType: String, | ||
| val pushStatus: String | ||
|
||
| ) | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -94,8 +94,12 @@ fun MyPageRoute( | |||||||||||||||
| rememberPermissionState(permission = notificationPermission) | ||||||||||||||||
| var isChecked by remember { | ||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 변수는 상태 객체 자체가 바뀌지 않고, 내부 값만 변경되는 구조라서 val로 선언해도 괜찮을 것 같아요!
Suggested change
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 말씀해주신 부분 보고 저도 생각을 해 보았는데요..! 사실 저는 다른 분들의 의견도 궁금하네요!!
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. var의 경우 상태 객체 자체를 바꿀 수 있어서 조금의 실수라도 방지하기 위해 val을 사용하는게 좋다고 생각했습니다!
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 저는 by에 한표 드립니당!! 가독성이 더 좋아지고 더 직관적인 것 같아서요!! 그리고 by 사용하면 var이어도 상태 객체 자체를 바꾸지는 못할걸요.....?(아마도... 그치만 아닐 수 있음 주의)
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 조금 더 찾아보니까 by를 사용하면 상태 값 자체에 접근할 수 있고 효빈이 말처럼 객체 자체를 바꾸진 못한다고 하네요! |
||||||||||||||||
| mutableStateOf( | ||||||||||||||||
| if (!permissionState.status.isGranted) false | ||||||||||||||||
| else viewModel.getAlarmAvailability() | ||||||||||||||||
| if (!permissionState.status.isGranted) { | ||||||||||||||||
| viewModel.updateAlarmAvailability(false) | ||||||||||||||||
| false | ||||||||||||||||
| } else { | ||||||||||||||||
| viewModel.getAlarmAvailability() | ||||||||||||||||
| } | ||||||||||||||||
| ) | ||||||||||||||||
| } | ||||||||||||||||
| val notificationSettingsLauncher = | ||||||||||||||||
|
|
@@ -116,7 +120,7 @@ fun MyPageRoute( | |||||||||||||||
| } | ||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 113부터 119가 예전 코드라 태그가 안되네요;;
Suggested change
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 오호 혹시 어떻게 합친다는 의미인지 여쭤봐도 될까용...??
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아! 코드를 제시해주셨군요!! 😅😅 반영해놓겠습니다~ 공부해올게요. |
||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| LaunchedEffect(key1 = true) { | ||||||||||||||||
| LaunchedEffect(Unit) { | ||||||||||||||||
| viewModel.getProfile() | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
|
|
||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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,35 @@ class MyPageViewModel @Inject constructor( | |
| private val _sideEffects = MutableSharedFlow<MyPageSideEffect>() | ||
| val sideEffects: SharedFlow<MyPageSideEffect> get() = _sideEffects.asSharedFlow() | ||
|
|
||
| private val debounceFlow = MutableSharedFlow<AlarmInfo>() | ||
|
|
||
| private val lastSuccessfulAlarmStatus = mutableMapOf<String, Boolean>() | ||
|
|
||
| init { | ||
| viewModelScope.launch { | ||
| debounceFlow | ||
| .groupBy { it.id } | ||
| .flatMapMerge { (_, flow) -> flow.debounce(DEBOUNCE_DURATION) } | ||
|
Comment on lines
+52
to
+55
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 확장함수 너무 좋네요👍 여러 아이템이 있을 때 각 아이템에 대한 디바운스를 걸기 위한 확장함수인 것으로 이해했는데 맞나요?? 근데 제가 이해한게 맞다면 하나의 목록만 관리하기 위해선
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 역시 예리하시네요!! 저도 처음엔 그렇게 생각해서 확장함수를 사용하지 않고 구현했었습니다! 단순히 한 화면에서 Optimistic UI를 구현하는 건 동작을 했었는데, 무슨 이유에서인지 처음 마이페이지 화면을 진입할 때는 바로 상태가 반영되지 않더라구여.. 두 번째로 화면에 진입해야 상태가 반영됐었습니다ㅜㅜ 이 부분에 대해서는 저도 더 알아봐야 될 것 같아요! 같이 공부해봐요🙌
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. => 확인 결과 하나의 목록일 때는 별도의 확장함수를 안 써줘도 되는 것 같아요! |
||
| .collect { info -> | ||
| val result = myPageRepository.updateAlarmState( | ||
| AlarmStatus(if (info.isAlarmAvailable) ENABLED.value else DISABLED.value) | ||
| ) | ||
|
|
||
| if (result.isSuccess) { | ||
| lastSuccessfulAlarmStatus[info.id] = info.isAlarmAvailable | ||
| } else { | ||
| val previous = lastSuccessfulAlarmStatus[info.id] ?: !info.isAlarmAvailable | ||
| _state.update { currentState -> | ||
| currentState.copy(pushStatus = 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,12 +119,15 @@ 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, | ||
| pushStatus = response.pushStatus | ||
| ) | ||
| } | ||
| }.onFailure { | ||
| _sideEffects.emit(MyPageSideEffect.ShowToast(DesignSystemR.string.server_failure)) | ||
| _state.value = _state.value.copy(isGetSuccess = UiState.Failure(it.toString())) | ||
|
|
@@ -133,7 +175,15 @@ class MyPageViewModel @Inject constructor( | |
| viewModelScope.launch { _sideEffects.emit(MyPageSideEffect.NavigateToProfileEdit) } | ||
|
|
||
| fun updateAlarmAvailability(availability: Boolean) { | ||
| _state.update { currentState -> | ||
| currentState.copy(pushStatus = 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 +196,8 @@ class MyPageViewModel @Inject constructor( | |
| } | ||
| } | ||
|
|
||
| } | ||
| companion object { | ||
| private const val DEBOUNCE_DURATION = 300L | ||
| private const val DEBOUNCE_KEY = "NOTIFICATION" | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| package com.terning.feature.mypage.mypage.model | ||
|
|
||
| internal data class AlarmInfo( | ||
| val id: String, | ||
| val isAlarmAvailable: Boolean | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
블로그 코드에선 예외처리를 해뒀던데 제외하신 이유가 있나요?!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
엇 이 부분은 제가 놓친 것 같네요! 구현하는 데에 집중하느라 예외처리에 신경을 못 썼네요ㅜㅜ 반영해놓겠습니다!!