Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
12 commits
Select commit Hold shift + click to select a range
2ec144a
[FEAT/#268] 임시저장 최초 사용 여부를 판단하기 위한 flag를 담을 SharedPreferences를 정의합니다.
SYAAINN Jun 4, 2025
affe7c3
[FEAT/#268] 일기 작성 화면에서 임시저장 다이얼로그의 임시저장을 버튼하면 flag를 true로 업데이트합니다.
SYAAINN Jun 4, 2025
829c091
[FEAT/#268] 홈화면에서 isFirstUse가 true일때 이어쓰기 알림설정 유도 바텀시트를 노출합니다.
SYAAINN Jun 4, 2025
5dc4ab1
[FEAT/#268] 바텀시트에서 알림 받기를 선택할 경우, 이어쓰기 알림 설정을 활성화하는 로직을 구현합니다.
SYAAINN Jun 4, 2025
f2ca24a
[CHORE/#268] 불필요한 파라미터를 제거합니다.
SYAAINN Jun 4, 2025
ab6eba0
[REFACTOR/#268] HomeScreen을 Recomposition해서 바텀시트 노출 조건 검사가 제대로 이루어지도록…
SYAAINN Jun 4, 2025
d3ecd2e
[CHORE/#268] 네트워크 연결 검사를 추가하고, 알림 시간 검증을 효과적으로 개선합니다.
SYAAINN Jun 4, 2025
d31441b
[CHORE/#268] 초기 알림 설정 화면에서는 이어쓰기 알림을 비활성화 합니다.
SYAAINN Jun 5, 2025
bdeebf9
[REFACTOR/#268] Local/Remote DataSource Module을 분리합니다.
SYAAINN Jun 6, 2025
b9d1818
[REFACTOR/#268] FcmTokenProvider을 이용하도록 변경합니다.
SYAAINN Jun 6, 2025
79c9a3e
[CHORE/#268] SharedPreferences의 property setter를 개선합니다.
SYAAINN Jun 6, 2025
116ac9e
[Refactor/#273] DraftRepository를 정의해서 Repository Pattern을 준수합니다.
SYAAINN Jun 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package com.sopt.clody.data.datastore

import android.content.SharedPreferences
import com.sopt.clody.di.qualifier.TokenPrefs
import javax.inject.Inject

class TokenDataStoreImpl @Inject constructor(
private val sharedPreferences: SharedPreferences,
@TokenPrefs private val sharedPreferences: SharedPreferences,
) : TokenDataStore {
override var accessToken: String
get() = sharedPreferences.getString(ACCESS_TOKEN, "") ?: ""
Expand Down
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"
}
}
18 changes: 18 additions & 0 deletions app/src/main/java/com/sopt/clody/di/SharedPreferencesModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ package com.sopt.clody.di

import android.content.Context
import android.content.SharedPreferences
import com.sopt.clody.data.local.datasource.FirstDraftLocalDataSource
import com.sopt.clody.data.local.datasourceimpl.FirstDraftLocalDataSourceImpl
import com.sopt.clody.di.qualifier.FirstDraftPrefs
import com.sopt.clody.di.qualifier.TokenPrefs
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
Expand All @@ -12,9 +16,23 @@ import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object SharedPreferencesModule {

@TokenPrefs
@Provides
@Singleton
fun provideSharedPreferences(@ApplicationContext context: Context): SharedPreferences {
return context.getSharedPreferences("token_prefs", Context.MODE_PRIVATE)
}

@FirstDraftPrefs
@Provides
@Singleton
fun provideFirstDraftSharedPreferences(@ApplicationContext context: Context): SharedPreferences {
return context.getSharedPreferences("first_draft_prefs", Context.MODE_PRIVATE)
}

@Provides
@Singleton
fun provideFirstDraftLocalDataSource(@FirstDraftPrefs sharedPreferences: SharedPreferences): FirstDraftLocalDataSource =
FirstDraftLocalDataSourceImpl(sharedPreferences)
}
3 changes: 2 additions & 1 deletion app/src/main/java/com/sopt/clody/di/TokenDataStoreModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.sopt.clody.di
import android.content.SharedPreferences
import com.sopt.clody.data.datastore.TokenDataStore
import com.sopt.clody.data.datastore.TokenDataStoreImpl
import com.sopt.clody.di.qualifier.TokenPrefs
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
Expand All @@ -14,7 +15,7 @@ import javax.inject.Singleton
object TokenDataStoreModule {
@Provides
@Singleton
fun provideTokenDataStore(sharedPreferences: SharedPreferences): TokenDataStore {
fun provideTokenDataStore(@TokenPrefs sharedPreferences: SharedPreferences): TokenDataStore {
return TokenDataStoreImpl(sharedPreferences)
}
}
11 changes: 11 additions & 0 deletions app/src/main/java/com/sopt/clody/di/qualifier/Qualifier.kt
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
Expand Up @@ -45,7 +45,7 @@ class TimeReminderViewModel @Inject constructor(

val requestDto = SendNotificationRequestDto(
isDiaryAlarm = isPermissionGranted,
isDraftAlarm = isPermissionGranted,
isDraftAlarm = false,
isReplyAlarm = isPermissionGranted,
time = selectedTime,
fcmToken = fcmToken,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -112,6 +123,72 @@ fun HomeRoute(
selectedYear = selectedYear,
selectedMonth = selectedMonth,
)

if (showFirstDraftPopup) {
ClodyPopupBottomSheet(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P4
Route는 UI 상태 구독과 조립만 담당하는게 좋지 않을까요? 물론 방학시즌에 홈화면 개편이 들어갈 예정이니 요건 고려해보시고 바꿔주시면 될 것 같습니다~

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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P4
요 친구를 Route에 둔 이유가 있을까요?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UI 상태 구독과 더불어 SideEffect 적인 것들은 Route에서 처리하는 것이 자연스럽다고 생각했습니다.

추후 MVI 구조를 적용한다고 생각했을때,
Screen에서 SideEffect를 트리거 -> Route에서 해당 SideEffect를 수집해서 토스트 노출

이 토스트 코드의 책임(?)을 다시 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() },
)
}
}
}

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -211,7 +288,7 @@ fun HomeScreen(
}

when (deleteDiaryState) {
is DeleteDiaryState.Idle -> { }
is DeleteDiaryState.Idle -> {}

is DeleteDiaryState.Loading -> {
LoadingScreen()
Expand Down
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
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
Expand All @@ -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,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2
이렇게 DataSource를 가져다써도 큰 문제는 없을 것 같긴 한데
viewmodel - repository pattern을 지키고 싶으면 아래와 같은 방법으로도 할 수 있을 것 같아요

interface DraftRepository {
    fun isFirstUse(): Boolean
    fun setFirstUse(value:Boolean)
}

class DraftRepositoryImpl @Inject Constructor
override fun isFirstUse(): Boolean= firstDraftLocalDataSource.isFirstUse

Copy link
Copy Markdown
Contributor Author

@SYAAINN SYAAINN Jun 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사실 DataSource를 가져다 쓰는 구조는 지금까지의 코드 구조(Repository Pattern)를 많이 해치는 거 같아 조금 찝찝하긴합니다. 그래서 패턴을 따르도록 개선을 해야할 것 같기는 한데,

여기서 고민이 되는 것은 현재 FirstDraftLocalDataSource는 1. 일기2. 임시저장하는 기능 처음 사용할 경우 3. 알림 설정을 돕기 위한 기능이다보니 .. DiaryRepository, NotificationRepository, DraftRepository(신규 생성) 중 어디로 들어가야할 지에 대한 것입니다!

DraftRepository를 새로 만들기에는 기존 두개에 충분히 들어갈만 하지 않을까하는 생각이 들고, Repository의 개수가 늘어날수록 의미가 퇴색된다고 생각이 돼서 혹시 어디로 들어가는게 좋을 것 같다고 생각하시나요?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

흠...저는 Repository구분을 할때 "책임" 단위로 분리를 해는 방법이 좋은 방법이라고 생각합니다.
그래서 의미 있는 독립된 도메인이라면 Repository 수는 늘어나도 괜찮다고 생각하거덩여.
DiaryRepository, NotificationRepository 에 넣는 케이스일 경우 서버API와 로컬 SharedPreferences가 섞이는 이슈가 발생 하겠죠 아마도...?
FirstDraftLocalDataSource는 기기 내부적 플래그 역할을 해서 독립적인 도메인의 의미를 갖는다. 라고 생각해서 분리하는 쪽으로 생각을 했습니다...!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DraftRepository를 만들면 뭔가 이름상 임시저장 조회/쓰기 API도 이쪽으로 옮겨야하지 않을까요? 이름을 바꿔야하나 ㅜ

제가 이해한 Repository Pattern으로는 로컬과 서버API가 섞이는 것은 필연적이라고 생각을 합니다. 데이터의 출처와 관계없이 하나의 Repository(저장소)에서 오는 data라고 속이는(?) 패턴이라고 생각해서요! 섞이면 발생하는 문제가 있을까요?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아 맞다 리포지토리는 local, remote datasource가 섞여도 된다는걸 간과하고 있었습니다ㅋㅋㅋ...
말씀하신대로 "임시"fetch/save를 아예 분리 할까요 그냥?

) : ViewModel() {

private val _calendarState = MutableStateFlow<CalendarState<MonthlyCalendarResponseDto>>(CalendarState.Idle)
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add network availability check for consistency.

The enableDraftAlarm function makes API calls but doesn't check network availability, unlike other functions in this ViewModel (loadCalendarData, loadDailyDiariesData). Add a network check for consistency and better error handling.

 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fun enableDraftAlarm(context: Context) {
viewModelScope.launch {
val fcmToken = getFcmToken(context) ?: return@launch
val notificationInfo = getNotificationInfo() ?: return@launch
val request = buildDraftAlarmRequest(notificationInfo, fcmToken)
sendDraftAlarmRequest(request)
}
}
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)
}
}
🤖 Prompt for AI Agents
In app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt
around lines 206 to 213, the enableDraftAlarm function lacks a network
availability check before making API calls. Add a check for network connectivity
at the start of the function, similar to the approach used in loadCalendarData
and loadDailyDiariesData, and handle the case where the network is unavailable
by returning early or notifying the user appropriately.


private suspend fun getFcmToken(context: Context): String? {
val token = getTokenFromPreferences(context)
if (token.isNullOrBlank()) {
_draftAlarmChangeState.value = NotificationChangeState.Failure("FCM Token을 가져오는데 실패했습니다.")
return null
}
return token
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Implement token retrieval locally to reduce coupling.

Instead of importing getTokenFromPreferences from a companion object, implement it locally like other ViewModels in the codebase do (see TimeReminderViewModel and NotificationSettingViewModel).

-private suspend fun getFcmToken(context: Context): String? {
-    val token = getTokenFromPreferences(context)
+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
 }

Also note that this function doesn't need to be suspend as it doesn't perform any suspending operations.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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.


private suspend fun getNotificationInfo(): NotificationInfoResponseDto? {
return notificationRepository.getNotificationInfo().getOrElse {
_draftAlarmChangeState.value = NotificationChangeState.Failure("알림 정보를 가져오는데 실패했습니다.")
null
}
}

private fun buildDraftAlarmRequest(
info: NotificationInfoResponseDto,
fcmToken: String,
): SendNotificationRequestDto = SendNotificationRequestDto(
isDiaryAlarm = info.isDiaryAlarm,
isDraftAlarm = true,
isReplyAlarm = info.isReplyAlarm,
time = info.time.ifEmpty { "21:30" },
fcmToken = fcmToken,
)

private suspend fun sendDraftAlarmRequest(request: SendNotificationRequestDto) {
notificationRepository.sendNotification(request).fold(
onSuccess = {
_draftAlarmEnableToast.value = true
_draftAlarmChangeState.value = NotificationChangeState.Success(it)
},
onFailure = {
_draftAlarmChangeState.value = NotificationChangeState.Failure("이어쓰기 알림 설정에 실패했습니다.")
},
)
}

fun resetDraftAlarmEnableToast() {
_draftAlarmEnableToast.value = false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,11 @@ fun WriteDiaryRoute(
onDismissLimitMessage = { viewModel.updateShowLimitMessage(false) },
onDismissEmptyFieldsMessage = { viewModel.updateShowEmptyFieldsMessage(false) },
onDismissFailureDialog = { viewModel.resetFailureDialog() },
onDismissExitDialog = { viewModel.updateShowExitDialog(false) },
onDismissExitDialog = {
viewModel.updateDraftUsage()
viewModel.updateShowExitDialog(false)
navigateToHome(year, month)
},
onConfirmExitDialog = {
viewModel.updateShowExitDialog(false)
navigateToPrevious()
Expand Down
Loading