-
Notifications
You must be signed in to change notification settings - Fork 0
[Feat/#268] 이어쓰기 알림 설정을 유도하는 기능을 구현합니다. #272
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 8 commits
2ec144a
affe7c3
829c091
5dc4ab1
f2ca24a
ab6eba0
d3ecd2e
d31441b
bdeebf9
b9d1818
79c9a3e
116ac9e
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,11 @@ | ||
| package com.sopt.clody.data.local.datasource | ||
|
|
||
| /** | ||
| * 임시 저장 최초 사용 여부 판단을 위한 SharedPreferences | ||
| * @property isDraftUsed 임시 저장 사용 여부 | ||
| * @property isFirstUse 임시 저장 최초 사용 여부 | ||
| */ | ||
| interface FirstDraftLocalDataSource { | ||
| var isDraftUsed: Boolean | ||
| var isFirstUse: Boolean | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| package com.sopt.clody.data.local.datasourceimpl | ||
|
|
||
| import android.content.SharedPreferences | ||
| import com.sopt.clody.data.local.datasource.FirstDraftLocalDataSource | ||
| import com.sopt.clody.di.qualifier.FirstDraftPrefs | ||
| import javax.inject.Inject | ||
|
|
||
| class FirstDraftLocalDataSourceImpl @Inject constructor( | ||
| @FirstDraftPrefs private val sharedPreferences: SharedPreferences, | ||
| ) : FirstDraftLocalDataSource { | ||
| override var isDraftUsed: Boolean | ||
| get() = sharedPreferences.getBoolean(IS_DRAFT_USED, false) | ||
| set(value) = sharedPreferences.edit().putBoolean(IS_DRAFT_USED, value).apply() | ||
|
|
||
| override var isFirstUse: Boolean | ||
| get() = sharedPreferences.getBoolean(IS_FIRST_USE, false) | ||
| set(value) = sharedPreferences.edit().putBoolean(IS_FIRST_USE, value).apply() | ||
|
|
||
| companion object { | ||
| private const val IS_DRAFT_USED = "IS_DRAFT_USED" | ||
| private const val IS_FIRST_USE = "IS_FIRST_USE" | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package com.sopt.clody.di.qualifier | ||
|
|
||
| import javax.inject.Qualifier | ||
|
|
||
| @Qualifier | ||
| @Retention(AnnotationRetention.BINARY) | ||
| annotation class TokenPrefs | ||
|
|
||
| @Qualifier | ||
| @Retention(AnnotationRetention.BINARY) | ||
| annotation class FirstDraftPrefs |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,30 +3,38 @@ package com.sopt.clody.presentation.ui.home.screen | |
| import android.app.Activity | ||
| import androidx.activity.compose.BackHandler | ||
| import androidx.compose.foundation.background | ||
| import androidx.compose.foundation.clickable | ||
| import androidx.compose.foundation.layout.Column | ||
| import androidx.compose.foundation.layout.Spacer | ||
| import androidx.compose.foundation.layout.fillMaxWidth | ||
| import androidx.compose.foundation.layout.height | ||
| import androidx.compose.foundation.layout.navigationBarsPadding | ||
| import androidx.compose.foundation.layout.padding | ||
| import androidx.compose.material3.Scaffold | ||
| import androidx.compose.material3.Text | ||
| import androidx.compose.runtime.Composable | ||
| import androidx.compose.runtime.LaunchedEffect | ||
| import androidx.compose.runtime.getValue | ||
| import androidx.compose.runtime.mutableStateOf | ||
| import androidx.compose.runtime.remember | ||
| 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.text.style.TextAlign | ||
| import androidx.compose.ui.unit.dp | ||
| import androidx.hilt.navigation.compose.hiltViewModel | ||
| import androidx.lifecycle.compose.collectAsStateWithLifecycle | ||
| import com.sopt.clody.R | ||
| import com.sopt.clody.domain.model.ReplyStatus | ||
| import com.sopt.clody.presentation.ui.component.FailureScreen | ||
| import com.sopt.clody.presentation.ui.component.LoadingScreen | ||
| import com.sopt.clody.presentation.ui.component.bottomsheet.DiaryDeleteSheet | ||
| import com.sopt.clody.presentation.ui.component.button.ClodyButton | ||
| import com.sopt.clody.presentation.ui.component.dialog.ClodyDialog | ||
| import com.sopt.clody.presentation.ui.component.popup.ClodyPopupBottomSheet | ||
| import com.sopt.clody.presentation.ui.component.timepicker.YearMonthPicker | ||
| import com.sopt.clody.presentation.ui.component.toast.ClodyToastMessage | ||
| import com.sopt.clody.presentation.ui.home.calendar.model.DiaryDateData | ||
| import com.sopt.clody.presentation.ui.home.component.DiaryStateButton | ||
| import com.sopt.clody.presentation.ui.home.component.HomeTopAppBar | ||
|
|
@@ -56,6 +64,9 @@ fun HomeRoute( | |
| val calendarState by homeViewModel.calendarState.collectAsStateWithLifecycle() | ||
| val dailyDiariesState by homeViewModel.dailyDiariesState.collectAsStateWithLifecycle() | ||
| val replyStatus by homeViewModel.replyStatus.collectAsStateWithLifecycle() | ||
| val showFirstDraftPopup by homeViewModel.showFirstDraftPopup.collectAsStateWithLifecycle() | ||
| val draftAlarmEnableToast by homeViewModel.draftAlarmEnableToast.collectAsStateWithLifecycle() | ||
| val context = LocalContext.current | ||
|
|
||
| val isError = calendarState is CalendarState.Error || dailyDiariesState is DailyDiariesState.Error | ||
| val errorMessage = when { | ||
|
|
@@ -112,6 +123,72 @@ fun HomeRoute( | |
| selectedYear = selectedYear, | ||
| selectedMonth = selectedMonth, | ||
| ) | ||
|
|
||
| if (showFirstDraftPopup) { | ||
| ClodyPopupBottomSheet( | ||
|
Contributor
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. P4 |
||
| onDismissRequest = { homeViewModel.updateFirstDraftUse(false) }, | ||
| content = { | ||
| Column( | ||
| horizontalAlignment = Alignment.CenterHorizontally, | ||
| modifier = Modifier | ||
| .fillMaxWidth() | ||
| .padding(top = 20.dp) | ||
| .padding(horizontal = 16.dp), | ||
| ) { | ||
| Text( | ||
| text = "기한이 지나면\n로디의 답장을 받을 수 없어요!", | ||
| color = ClodyTheme.colors.gray01, | ||
| textAlign = TextAlign.Center, | ||
| style = ClodyTheme.typography.head3, | ||
| ) | ||
| Spacer(modifier = Modifier.height(10.dp)) | ||
| Text( | ||
| text = "답장 마감 전에 일기를 이어쓸 수 있도록\n알려드리기 위해서는 알림 설정이 필요해요.", | ||
| color = ClodyTheme.colors.gray04, | ||
| textAlign = TextAlign.Center, | ||
| style = ClodyTheme.typography.body3Regular, | ||
| ) | ||
| Spacer(modifier = Modifier.height(4.dp)) | ||
| Text( | ||
| text = "[설정 > 애플리케이션 > 클로디 > 알림 > 알림표시]", | ||
| color = ClodyTheme.colors.gray04, | ||
| textAlign = TextAlign.Center, | ||
| style = ClodyTheme.typography.body3Regular, | ||
| ) | ||
| Spacer(modifier = Modifier.height(28.dp)) | ||
| ClodyButton( | ||
| text = "알림 받기", | ||
| onClick = { | ||
| homeViewModel.enableDraftAlarm(context) | ||
| homeViewModel.updateFirstDraftUse(false) | ||
| }, | ||
| enabled = true, | ||
| modifier = Modifier.fillMaxWidth(), | ||
| ) | ||
| Text( | ||
| text = "다음에 하기", | ||
| modifier = Modifier | ||
| .clickable(onClick = { homeViewModel.updateFirstDraftUse(false) }) | ||
| .padding(12.dp), | ||
| color = ClodyTheme.colors.gray05, | ||
| style = ClodyTheme.typography.body4Medium, | ||
| ) | ||
| Spacer(modifier = Modifier.height(4.dp)) | ||
| } | ||
| }, | ||
| ) | ||
| } | ||
|
|
||
| if (draftAlarmEnableToast) { | ||
| ClodyToastMessage( | ||
|
Comment on lines
+182
to
+183
Contributor
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. P4
Contributor
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. UI 상태 구독과 더불어 SideEffect 적인 것들은 Route에서 처리하는 것이 자연스럽다고 생각했습니다. 추후 MVI 구조를 적용한다고 생각했을때, 이 토스트 코드의 책임(?)을 다시 Screen에서 지는게 왔다리갔다리..? 같이 생각이 돼서 UiState가 성공한 화면에 대해서만 Screen에 정의하고 그 외는 Route에 위치시켰습니다. 그런데 지금 생각하니 또 확신이 잘 안서기는 하네요. HomeScreen이 리팩토링할 부분이 워낙 많아서 ..! Route에서 져야할 책임과 Screen에서 져야하는 책임은 무엇일까요? |
||
| message = "이어쓰기 알림 설정을 완료했어요.", | ||
| iconResId = R.drawable.ic_toast_check_on_18, | ||
| backgroundColor = ClodyTheme.colors.gray04, | ||
| contentColor = ClodyTheme.colors.white, | ||
| durationMillis = 3000, | ||
| onDismiss = { homeViewModel.resetDraftAlarmEnableToast() }, | ||
| ) | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -183,7 +260,7 @@ fun HomeScreen( | |
| containerColor = ClodyTheme.colors.white, | ||
| content = { innerPadding -> | ||
| when (val state = calendarState) { | ||
| is CalendarState.Idle -> { } | ||
| is CalendarState.Idle -> {} | ||
|
|
||
| is CalendarState.Loading -> { | ||
| LoadingScreen() | ||
|
|
@@ -211,7 +288,7 @@ fun HomeScreen( | |
| } | ||
|
|
||
| when (deleteDiaryState) { | ||
| is DeleteDiaryState.Idle -> { } | ||
| is DeleteDiaryState.Idle -> {} | ||
|
|
||
| is DeleteDiaryState.Loading -> { | ||
| LoadingScreen() | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,12 +1,19 @@ | ||||||||||||||||||||||||||||||||||||||||||
| package com.sopt.clody.presentation.ui.home.screen | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| import android.content.Context | ||||||||||||||||||||||||||||||||||||||||||
| import androidx.lifecycle.ViewModel | ||||||||||||||||||||||||||||||||||||||||||
| import androidx.lifecycle.viewModelScope | ||||||||||||||||||||||||||||||||||||||||||
| import com.sopt.clody.ClodyFirebaseMessagingService.Companion.getTokenFromPreferences | ||||||||||||||||||||||||||||||||||||||||||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||
| import com.sopt.clody.data.local.datasource.FirstDraftLocalDataSource | ||||||||||||||||||||||||||||||||||||||||||
| import com.sopt.clody.data.remote.dto.request.SendNotificationRequestDto | ||||||||||||||||||||||||||||||||||||||||||
| import com.sopt.clody.data.remote.dto.response.DailyDiariesResponseDto | ||||||||||||||||||||||||||||||||||||||||||
| import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto | ||||||||||||||||||||||||||||||||||||||||||
| import com.sopt.clody.data.remote.dto.response.NotificationInfoResponseDto | ||||||||||||||||||||||||||||||||||||||||||
| import com.sopt.clody.data.remote.util.NetworkUtil | ||||||||||||||||||||||||||||||||||||||||||
| import com.sopt.clody.domain.repository.DiaryRepository | ||||||||||||||||||||||||||||||||||||||||||
| import com.sopt.clody.domain.repository.NotificationRepository | ||||||||||||||||||||||||||||||||||||||||||
| import com.sopt.clody.presentation.ui.home.calendar.model.DiaryDateData | ||||||||||||||||||||||||||||||||||||||||||
| import com.sopt.clody.presentation.ui.setting.notificationsetting.screen.NotificationChangeState | ||||||||||||||||||||||||||||||||||||||||||
| import com.sopt.clody.presentation.utils.network.ErrorMessages | ||||||||||||||||||||||||||||||||||||||||||
| import dagger.hilt.android.lifecycle.HiltViewModel | ||||||||||||||||||||||||||||||||||||||||||
| import kotlinx.coroutines.flow.MutableStateFlow | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -18,7 +25,9 @@ import javax.inject.Inject | |||||||||||||||||||||||||||||||||||||||||
| @HiltViewModel | ||||||||||||||||||||||||||||||||||||||||||
| class HomeViewModel @Inject constructor( | ||||||||||||||||||||||||||||||||||||||||||
| private val diaryRepository: DiaryRepository, | ||||||||||||||||||||||||||||||||||||||||||
| private val notificationRepository: NotificationRepository, | ||||||||||||||||||||||||||||||||||||||||||
| private val networkUtil: NetworkUtil, | ||||||||||||||||||||||||||||||||||||||||||
| private val firstDraftLocalDataSource: FirstDraftLocalDataSource, | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
| ) : ViewModel() { | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| private val _calendarState = MutableStateFlow<CalendarState<MonthlyCalendarResponseDto>>(CalendarState.Idle) | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -62,6 +71,15 @@ class HomeViewModel @Inject constructor( | |||||||||||||||||||||||||||||||||||||||||
| private val _showDiaryDeleteDialog = MutableStateFlow(false) | ||||||||||||||||||||||||||||||||||||||||||
| val showDiaryDeleteDialog: StateFlow<Boolean> get() = _showDiaryDeleteDialog | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| private val _showFirstDraftPopup = MutableStateFlow(firstDraftLocalDataSource.isFirstUse) | ||||||||||||||||||||||||||||||||||||||||||
| val showFirstDraftPopup: StateFlow<Boolean> = _showFirstDraftPopup | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| private val _draftAlarmChangeState = MutableStateFlow<NotificationChangeState>(NotificationChangeState.Idle) | ||||||||||||||||||||||||||||||||||||||||||
| val draftAlarmChangeState: StateFlow<NotificationChangeState> = _draftAlarmChangeState | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| private val _draftAlarmEnableToast = MutableStateFlow(false) | ||||||||||||||||||||||||||||||||||||||||||
| val draftAlarmEnableToast: StateFlow<Boolean> = _draftAlarmEnableToast | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| private val _errorState = MutableStateFlow<Pair<Boolean, String>>(false to "") | ||||||||||||||||||||||||||||||||||||||||||
| val errorState: StateFlow<Pair<Boolean, String>> = _errorState | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -179,4 +197,66 @@ class HomeViewModel @Inject constructor( | |||||||||||||||||||||||||||||||||||||||||
| fun setShowDiaryDeleteDialog(state: Boolean) { | ||||||||||||||||||||||||||||||||||||||||||
| _showDiaryDeleteDialog.value = state | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| fun updateFirstDraftUse(newState: Boolean) { | ||||||||||||||||||||||||||||||||||||||||||
| firstDraftLocalDataSource.isFirstUse = newState | ||||||||||||||||||||||||||||||||||||||||||
| _showFirstDraftPopup.value = newState | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| fun enableDraftAlarm(context: Context) { | ||||||||||||||||||||||||||||||||||||||||||
| viewModelScope.launch { | ||||||||||||||||||||||||||||||||||||||||||
| if (!networkUtil.isNetworkAvailable()) { | ||||||||||||||||||||||||||||||||||||||||||
| setErrorState(true, ErrorMessages.FAILURE_NETWORK_MESSAGE) | ||||||||||||||||||||||||||||||||||||||||||
| return@launch | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| val fcmToken = getFcmToken(context) ?: return@launch | ||||||||||||||||||||||||||||||||||||||||||
| val notificationInfo = getNotificationInfo() ?: return@launch | ||||||||||||||||||||||||||||||||||||||||||
| val request = buildDraftAlarmRequest(notificationInfo, fcmToken) | ||||||||||||||||||||||||||||||||||||||||||
| sendDraftAlarmRequest(request) | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+206
to
+219
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. Add network availability check for consistency. The fun enableDraftAlarm(context: Context) {
viewModelScope.launch {
+ if (!networkUtil.isNetworkAvailable()) {
+ _draftAlarmChangeState.value = NotificationChangeState.Failure("네트워크 연결을 확인해주세요.")
+ return@launch
+ }
val fcmToken = getFcmToken(context) ?: return@launch
val notificationInfo = getNotificationInfo() ?: return@launch
val request = buildDraftAlarmRequest(notificationInfo, fcmToken)
sendDraftAlarmRequest(request)
}
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| private suspend fun getFcmToken(context: Context): String? { | ||||||||||||||||||||||||||||||||||||||||||
SYAAINN marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||
| val token = getTokenFromPreferences(context) | ||||||||||||||||||||||||||||||||||||||||||
| if (token.isNullOrBlank()) { | ||||||||||||||||||||||||||||||||||||||||||
| _draftAlarmChangeState.value = NotificationChangeState.Failure("FCM Token을 가져오는데 실패했습니다.") | ||||||||||||||||||||||||||||||||||||||||||
| return null | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| return token | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
| private suspend fun getFcmToken(context: Context): String? { | |
| val token = getTokenFromPreferences(context) | |
| if (token.isNullOrBlank()) { | |
| _draftAlarmChangeState.value = NotificationChangeState.Failure("FCM Token을 가져오는데 실패했습니다.") | |
| return null | |
| } | |
| return token | |
| } | |
| private fun getFcmToken(context: Context): String? { | |
| val sharedPreferences = context.getSharedPreferences("fcm_prefs", Context.MODE_PRIVATE) | |
| val token = sharedPreferences.getString("fcm_token", null) | |
| if (token.isNullOrBlank()) { | |
| _draftAlarmChangeState.value = NotificationChangeState.Failure("FCM Token을 가져오는데 실패했습니다.") | |
| return null | |
| } | |
| return token | |
| } |
🤖 Prompt for AI Agents
In app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt
around lines 215 to 222, the getFcmToken function currently calls
getTokenFromPreferences from a companion object, creating unnecessary coupling,
and is marked suspend without needing to be. Refactor by implementing the token
retrieval logic locally within this ViewModel, similar to the approach in
TimeReminderViewModel and NotificationSettingViewModel, and remove the suspend
modifier since no suspending operations are performed.
Uh oh!
There was an error while loading. Please reload this page.