From 710a1986a5d2a773cff361ec8846ef32cf0f266d Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Mon, 11 Aug 2025 20:48:34 +0900 Subject: [PATCH 01/14] =?UTF-8?q?[REFACTOR/#317]=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EA=B6=8C=ED=95=9C=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 알림 권한 요청 로직 삭제 (TimeReminderScreen) - Home 화면 진입 시 알림 권한 확인하도록 수정 (HomeScreen) --- .../auth/timereminder/TimeReminderScreen.kt | 31 ++---------------- .../presentation/ui/home/screen/HomeScreen.kt | 32 +++++++++++++++++++ .../ui/home/screen/HomeViewModel.kt | 6 ++++ 3 files changed, 40 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderScreen.kt index a00ce254..5f17df3f 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderScreen.kt @@ -1,10 +1,5 @@ package com.sopt.clody.presentation.ui.auth.timereminder -import android.Manifest -import android.content.pm.PackageManager -import android.os.Build -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -32,7 +27,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.core.content.ContextCompat import androidx.hilt.navigation.compose.hiltViewModel import com.sopt.clody.R import com.sopt.clody.presentation.ui.auth.component.container.PickerBox @@ -57,27 +51,6 @@ fun TimeReminderRoute( var showDialog by remember { mutableStateOf(false) } var dialogMessage by remember { mutableStateOf("") } - val isNotificationPermissionGranted = remember { mutableStateOf(false) } - - val requestPermissionLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.RequestPermission(), - ) { isGranted: Boolean -> - isNotificationPermissionGranted.value = isGranted - } - // 알림 권한 요청 - LaunchedEffect(Unit) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - val notificationPermission = Manifest.permission.POST_NOTIFICATIONS - if (ContextCompat.checkSelfPermission(context, notificationPermission) != PackageManager.PERMISSION_GRANTED) { - requestPermissionLauncher.launch(notificationPermission) - } else { - isNotificationPermissionGranted.value = true - } - } else { - isNotificationPermissionGranted.value = true - } - } - // 알림 권한 요청 결과에 따른 처리 LaunchedEffect(timeReminderState) { when (val result = timeReminderState) { @@ -103,14 +76,14 @@ fun TimeReminderRoute( TimeReminderScreen( onStartClick = { viewModel.setSelectedTime(TimePeriod.PM, "9", "30") - viewModel.sendNotification(context, isNotificationPermissionGranted.value) + viewModel.sendNotification(context, true) }, onTimeSelected = { period, hour, minute -> viewModel.setSelectedTime(period, hour, minute) }, onCompleteClick = { AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.ONBOARDING_ALARM) - viewModel.sendNotification(context, isNotificationPermissionGranted.value) + viewModel.sendNotification(context, true) }, isLoading = timeReminderState is TimeReminderState.Loading, ) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt index a5f9d9a0..77341064 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt @@ -1,7 +1,12 @@ package com.sopt.clody.presentation.ui.home.screen +import android.Manifest import android.app.Activity +import android.content.pm.PackageManager +import android.os.Build import androidx.activity.compose.BackHandler +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box @@ -26,6 +31,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.sopt.clody.R @@ -84,6 +90,16 @@ fun HomeRoute( val showYearMonthPickerState by homeViewModel.showYearMonthPickerState.collectAsStateWithLifecycle() val hasDraft by homeViewModel.hasDraft.collectAsStateWithLifecycle() + // 알림 권한 관련 상태 + val isNotificationPermissionGranted = remember { mutableStateOf(false) } + + val requestPermissionLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission(), + ) { isGranted: Boolean -> + isNotificationPermissionGranted.value = isGranted + homeViewModel.updateNotificationPermissionGranted(isGranted) + } + LaunchedEffect(Unit) { AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.HOME) @@ -93,6 +109,22 @@ fun HomeRoute( } } + // 알림 권한 요청 + LaunchedEffect(Unit) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + val notificationPermission = Manifest.permission.POST_NOTIFICATIONS + if (ContextCompat.checkSelfPermission(context, notificationPermission) != PackageManager.PERMISSION_GRANTED) { + requestPermissionLauncher.launch(notificationPermission) + } else { + isNotificationPermissionGranted.value = true + homeViewModel.updateNotificationPermissionGranted(true) + } + } else { + isNotificationPermissionGranted.value = true + homeViewModel.updateNotificationPermissionGranted(true) + } + } + LaunchedEffect(Unit) { val year = selectedDiaryDate.year val month = selectedDiaryDate.month diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt index fc5121fb..cdb558d1 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt @@ -99,6 +99,8 @@ class HomeViewModel @Inject constructor( private val _hasDraft = MutableStateFlow(false) val hasDraft: StateFlow get() = _hasDraft + private val _isNotificationPermissionGranted = MutableStateFlow(false) + private var isInitialized = false init { @@ -312,4 +314,8 @@ class HomeViewModel @Inject constructor( reviewRepository.setShouldShowPopup(state) _showInAppReviewPopup.value = state } + + fun updateNotificationPermissionGranted(isGranted: Boolean) { + _isNotificationPermissionGranted.value = isGranted + } } From 7a956e3737e6a1bce29d86a296f9f2352ebce56a Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Mon, 11 Aug 2025 21:31:07 +0900 Subject: [PATCH 02/14] =?UTF-8?q?[REFACTOR/#319]=20=ED=99=88=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EB=A1=9C=EB=94=A9=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20=EB=8F=99?= =?UTF-8?q?=EC=8B=9C=EC=84=B1=20=EA=B4=80=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 홈 화면 진입 시 캘린더 데이터와 일일 일기 데이터를 병렬로 로드하도록 수정 - 데이터 로딩 시 Mutex를 사용하여 동시성 문제 방지 - 중복 데이터 로딩 방지 로직 추가 - `updateSelectedDiaryDate` 함수 제거 및 관련 로직 `updateYearMonthAndLoadData` 함수로 통합 - 네트워크 요청 시 `withContext(Dispatchers.IO)` 사용하여 백그라운드 스레드에서 실행되도록 수정 --- .../presentation/ui/home/screen/HomeScreen.kt | 46 ++--- .../ui/home/screen/HomeViewModel.kt | 177 +++++++++++------- 2 files changed, 127 insertions(+), 96 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt index 77341064..aae42876 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt @@ -46,7 +46,6 @@ 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 import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints @@ -55,8 +54,6 @@ import com.sopt.clody.presentation.utils.extension.toLocalizedMonthLabel import com.sopt.clody.presentation.utils.extension.toLocalizedYearLabel import com.sopt.clody.presentation.utils.navigation.Route import com.sopt.clody.ui.theme.ClodyTheme -import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope import java.time.LocalDate @Composable @@ -126,31 +123,18 @@ fun HomeRoute( } LaunchedEffect(Unit) { - val year = selectedDiaryDate.year - val month = selectedDiaryDate.month - val day = selectedDate.dayOfMonth - - try { - coroutineScope { - val calendarDeferred = async { homeViewModel.loadCalendarData(year, month) } - val dailyDeferred = async { homeViewModel.loadDailyDiariesData(year, month, day) } - - calendarDeferred.await() - dailyDeferred.await() - } - } catch (e: Exception) { - homeViewModel.setErrorState(true, "데이터를 불러오는데 실패했습니다.") - } + homeViewModel.updateYearMonthAndLoadData( + selectedDiaryDate.year, + selectedDiaryDate.month, + selectedDate.dayOfMonth, + ) } + if (isError) { FailureScreen( message = errorMessage, confirmAction = { - homeViewModel.refreshCalendarDataCalendarData( - selectedDiaryDate.year, - selectedDiaryDate.month, - ) - homeViewModel.loadDailyDiariesData( + homeViewModel.updateYearMonthAndLoadData( selectedDiaryDate.year, selectedDiaryDate.month, selectedDate.dayOfMonth, @@ -305,10 +289,11 @@ fun HomeRoute( confirmOption = stringResource(R.string.dialog_diary_delete_confirm), dismissOption = stringResource(R.string.dialog_diary_delete_dismiss), confirmAction = { + val d = homeViewModel.selectedDate.value homeViewModel.deleteDailyDiary( - selectedDiaryDate.year, - selectedDiaryDate.month, - selectedDate.dayOfMonth, + d.year, + d.monthValue, + d.dayOfMonth, ) homeViewModel.setShowDiaryDeleteDialog(false) }, @@ -352,8 +337,7 @@ fun HomeScreen( FailureScreen( message = errorMessage, confirmAction = { - homeViewModel.refreshCalendarDataCalendarData(selectedYear, selectedMonth) - homeViewModel.loadDailyDiariesData(selectedYear, selectedMonth, selectedDate.dayOfMonth) + homeViewModel.updateYearMonthAndLoadData(selectedYear, selectedMonth, selectedDate.dayOfMonth) }, ) } else { @@ -420,8 +404,7 @@ fun HomeScreen( LoadingScreen() } - is DeleteDiaryState.Success -> { - } + is DeleteDiaryState.Success -> {} is DeleteDiaryState.Failure -> { homeViewModel.setErrorState(true, stringResource(R.string.home_error_delete_diary)) @@ -465,8 +448,7 @@ fun HomeScreen( selectedYear = selectedYear, selectedMonth = selectedMonth, onYearMonthSelected = { year, month -> - homeViewModel.updateSelectedDiaryDate(DiaryDateData(year, month)) - homeViewModel.loadCalendarData(year, month) + homeViewModel.updateYearMonthAndLoadData(year, month) }, ) } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt index cdb558d1..aecb32b7 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt @@ -18,10 +18,17 @@ 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.ErrorMessageProvider import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext import java.time.LocalDate import java.time.ZoneId import javax.inject.Inject @@ -40,9 +47,8 @@ class HomeViewModel @Inject constructor( private val _calendarState = MutableStateFlow>(CalendarState.Idle) val calendarState: StateFlow> get() = _calendarState - private val _dailyDiariesState = MutableStateFlow>( - DailyDiariesState.Idle, - ) + private val _dailyDiariesState = + MutableStateFlow>(DailyDiariesState.Idle) val dailyDiariesState: StateFlow> get() = _dailyDiariesState private val _deleteDiaryState = MutableStateFlow(DeleteDiaryState.Idle) @@ -84,7 +90,8 @@ class HomeViewModel @Inject constructor( private val _showFirstDraftPopup = MutableStateFlow(draftRepository.getIsFirstUse()) val showFirstDraftPopup: StateFlow = _showFirstDraftPopup - private val _draftAlarmChangeState = MutableStateFlow(NotificationChangeState.Idle) + private val _draftAlarmChangeState = + MutableStateFlow(NotificationChangeState.Idle) val draftAlarmChangeState: StateFlow = _draftAlarmChangeState private val _draftAlarmEnableToast = MutableStateFlow(false) @@ -93,7 +100,7 @@ class HomeViewModel @Inject constructor( private val _showInAppReviewPopup = MutableStateFlow(reviewRepository.getShouldShowPopup()) val showInAppReviewPopup: StateFlow get() = _showInAppReviewPopup - private val _errorState = MutableStateFlow>(false to "") + private val _errorState = MutableStateFlow(false to "") val errorState: StateFlow> = _errorState private val _hasDraft = MutableStateFlow(false) @@ -111,6 +118,7 @@ class HomeViewModel @Inject constructor( if (!isInitialized) { val now = LocalDate.now() _selectedDiaryDate.value = DiaryDateData(now.year, now.monthValue) + _selectedDate.value = now isInitialized = true } } @@ -119,58 +127,57 @@ class HomeViewModel @Inject constructor( _errorState.value = isError to message } - fun loadCalendarData(year: Int, month: Int) { - viewModelScope.launch { - if (!isNetworkAvailable()) { - setErrorState(true, errorMessageProvider.getNetworkError()) - return@launch - } - - _calendarState.value = CalendarState.Loading - val result = diaryRepository.getMonthlyCalendarData(year, month) - _calendarState.value = result.fold( - onSuccess = { - setErrorState(false) - CalendarState.Success(it) - }, - onFailure = { exception -> - setErrorState(true, errorMessageProvider.getTemporaryError()) - CalendarState.Error(errorMessageProvider.getTemporaryError()) - }, - ) + private suspend fun loadCalendarData(year: Int, month: Int) { + if (!isNetworkAvailable()) { + setErrorState(true, errorMessageProvider.getNetworkError()) + return } + _calendarState.value = CalendarState.Loading + val result = withContext(Dispatchers.IO) { + diaryRepository.getMonthlyCalendarData(year, month) + } + _calendarState.value = result.fold( + onSuccess = { + setErrorState(false) + CalendarState.Success(it) + }, + onFailure = { + setErrorState(true, errorMessageProvider.getTemporaryError()) + CalendarState.Error(errorMessageProvider.getTemporaryError()) + }, + ) } - fun loadDailyDiariesData(year: Int, month: Int, date: Int) { - viewModelScope.launch { - if (!isNetworkAvailable()) { - setErrorState(true, errorMessageProvider.getNetworkError()) - return@launch - } - - _dailyDiariesState.value = DailyDiariesState.Loading - val result = diaryRepository.getDailyDiariesData(year, month, date) - _dailyDiariesState.value = result.fold( - onSuccess = { dailyResponse -> - _hasDraft.value = dailyResponse.isDraft - _diaryCount.value = dailyResponse.diaries.size - _isDeleted.value = dailyResponse.isDeleted - - setErrorState(false) - DailyDiariesState.Success(dailyResponse) - }, - onFailure = { exception -> - setErrorState(true, errorMessageProvider.getTemporaryError()) - DailyDiariesState.Error(errorMessageProvider.getTemporaryError()) - }, - ) + private suspend fun loadDailyDiariesData(year: Int, month: Int, date: Int) { + if (!isNetworkAvailable()) { + setErrorState(true, errorMessageProvider.getNetworkError()) + return + } + _dailyDiariesState.value = DailyDiariesState.Loading + val result = withContext(Dispatchers.IO) { + diaryRepository.getDailyDiariesData(year, month, date) } + _dailyDiariesState.value = result.fold( + onSuccess = { dailyResponse -> + _hasDraft.value = dailyResponse.isDraft + _diaryCount.value = dailyResponse.diaries.size + _isDeleted.value = dailyResponse.isDeleted + setErrorState(false) + DailyDiariesState.Success(dailyResponse) + }, + onFailure = { + setErrorState(true, errorMessageProvider.getTemporaryError()) + DailyDiariesState.Error(errorMessageProvider.getTemporaryError()) + }, + ) } fun deleteDailyDiary(year: Int, month: Int, day: Int) { viewModelScope.launch { _deleteDiaryResult.value = DeleteDiaryState.Loading - val result = diaryRepository.deleteDailyDiary(year, month, day) + val result = withContext(Dispatchers.IO) { + diaryRepository.deleteDailyDiary(year, month, day) + } _deleteDiaryResult.value = result.fold( onSuccess = { loadCalendarData(year, month) @@ -178,7 +185,6 @@ class HomeViewModel @Inject constructor( _diaryCount.value = 0 _isDeleted.value = false _replyStatus.value = ReplyStatus.UNREADY - DeleteDiaryState.Success }, onFailure = { @@ -188,24 +194,64 @@ class HomeViewModel @Inject constructor( } } - fun refreshCalendarDataCalendarData(year: Int, month: Int) { - if (calendarState.value is CalendarState.Success && - _selectedDiaryDate.value.year == year && - _selectedDiaryDate.value.month == month - ) { - return + private val loadDataMutex = Mutex() + fun updateYearMonthAndLoadData(year: Int, month: Int) { + viewModelScope.launch { + loadDataMutex.withLock { + val sameYm = _selectedDiaryDate.value.year == year && + _selectedDiaryDate.value.month == month + + val calendarLoaded = calendarState.value is CalendarState.Success + val dailyLoaded = dailyDiariesState.value is DailyDiariesState.Success + val selectedIsFirst = _selectedDate.value.dayOfMonth == 1 + val alreadyLoaded = sameYm && calendarLoaded && (selectedIsFirst && dailyLoaded) + + if (alreadyLoaded) return@withLock + + _selectedDiaryDate.value = DiaryDateData(year, month) + _selectedDate.value = LocalDate.of(year, month, 1) + + coroutineScope { + awaitAll( + async { loadCalendarData(year, month) }, + async { loadDailyDiariesData(year, month, 1) }, + ) + } + } } - _selectedDiaryDate.value = DiaryDateData(year, month) - loadCalendarData(year, month) } - fun updateSelectedDate(date: LocalDate) { - _selectedDate.value = date - loadDailyDiariesData(date.year, date.monthValue, date.dayOfMonth) + fun updateYearMonthAndLoadData(year: Int, month: Int, day: Int) { + viewModelScope.launch { + loadDataMutex.withLock { + val sameYmd = _selectedDiaryDate.value.year == year && + _selectedDiaryDate.value.month == month && + _selectedDate.value.dayOfMonth == day + + val calendarLoaded = calendarState.value is CalendarState.Success + val dailyLoaded = dailyDiariesState.value is DailyDiariesState.Success + val alreadyLoaded = sameYmd && calendarLoaded && dailyLoaded + + if (alreadyLoaded) return@withLock + + _selectedDiaryDate.value = DiaryDateData(year, month) + _selectedDate.value = LocalDate.of(year, month, day) + + coroutineScope { + awaitAll( + async { loadCalendarData(year, month) }, + async { loadDailyDiariesData(year, month, day) }, + ) + } + } + } } - fun updateSelectedDiaryDate(diaryDate: DiaryDateData) { - _selectedDiaryDate.value = diaryDate + fun updateSelectedDate(date: LocalDate) { + _selectedDate.value = date + viewModelScope.launch { + loadDailyDiariesData(date.year, date.monthValue, date.dayOfMonth) + } } fun updateDiaryState(diaries: List) { @@ -295,13 +341,16 @@ class HomeViewModel @Inject constructor( ) private suspend fun sendDraftAlarmRequest(request: SendNotificationRequestDto) { - notificationRepository.sendNotification(request).fold( + withContext(Dispatchers.IO) { + notificationRepository.sendNotification(request) + }.fold( onSuccess = { _draftAlarmEnableToast.value = true _draftAlarmChangeState.value = NotificationChangeState.Success(it) }, onFailure = { - _draftAlarmChangeState.value = NotificationChangeState.Failure(errorMessageProvider.getTemporaryError()) + _draftAlarmChangeState.value = + NotificationChangeState.Failure(errorMessageProvider.getTemporaryError()) }, ) } From dbfb2917744f217bd9d776d754c43fe3765aeeb1 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Thu, 14 Aug 2025 21:16:27 +0900 Subject: [PATCH 03/14] =?UTF-8?q?[MOD/#317]=20=EB=B2=84=EC=A0=84=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0812d8ce..c5627aeb 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -25,8 +25,8 @@ android { applicationId = "com.sopt.clody" minSdk = 28 targetSdk = 35 - versionCode = 28 - versionName = "1.4.0" + versionCode = 29 + versionName = "1.5.0" val kakaoApiKey: String = properties.getProperty("kakao.api.key") val amplitudeApiKey: String = properties.getProperty("amplitude.api.key") val googleAdmobAppId: String = properties.getProperty("GOOGLE_ADMOB_APP_ID", "") From fdd6da5296e1a374af10eeac23e67b0902cf4d6c Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Thu, 14 Aug 2025 21:16:43 +0900 Subject: [PATCH 04/14] =?UTF-8?q?[CHORE/#317]=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=20=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EC=82=AD?= =?UTF-8?q?=E3=85=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/auth/timereminder/TimeReminderScreen.kt | 4 ++-- .../ui/auth/timereminder/TimeReminderViewModel.kt | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderScreen.kt index 5f17df3f..2c536ff8 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderScreen.kt @@ -76,14 +76,14 @@ fun TimeReminderRoute( TimeReminderScreen( onStartClick = { viewModel.setSelectedTime(TimePeriod.PM, "9", "30") - viewModel.sendNotification(context, true) + viewModel.sendNotification(context) }, onTimeSelected = { period, hour, minute -> viewModel.setSelectedTime(period, hour, minute) }, onCompleteClick = { AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.ONBOARDING_ALARM) - viewModel.sendNotification(context, true) + viewModel.sendNotification(context) }, isLoading = timeReminderState is TimeReminderState.Loading, ) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderViewModel.kt index 07fba0d3..ff1c41ae 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderViewModel.kt @@ -33,7 +33,7 @@ class TimeReminderViewModel @Inject constructor( var selectedTime by mutableStateOf("21:30") private set - fun sendNotification(context: Context, isPermissionGranted: Boolean) { + fun sendNotification(context: Context) { viewModelScope.launch { if (networkConnectivityObserver.networkStatus.first() == NetworkStatus.Unavailable) { _timeReminderState.value = TimeReminderState.Failure(errorMessageProvider.getNetworkError()) @@ -47,9 +47,9 @@ class TimeReminderViewModel @Inject constructor( } val requestDto = SendNotificationRequestDto( - isDiaryAlarm = isPermissionGranted, + isDiaryAlarm = true, isDraftAlarm = false, - isReplyAlarm = isPermissionGranted, + isReplyAlarm = true, time = selectedTime, fcmToken = fcmToken, ) From 2dd1d8fd551536794bb53f8c84d69d8097522d84 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Thu, 14 Aug 2025 21:17:01 +0900 Subject: [PATCH 05/14] =?UTF-8?q?[REFACTOR/#317]=20=EB=AF=B8=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=20=EB=B3=80=EC=88=98=20=EC=82=AD=EC=A0=9C=20=ED=9B=84?= =?UTF-8?q?=20=ED=99=88=ED=99=94=EB=A9=B4=EC=97=90=EC=84=9C=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EA=B6=8C=ED=95=9C=20=EC=9A=94=EC=B2=AD=20=EC=8A=B9?= =?UTF-8?q?=EC=9D=B8=20=EC=97=AC=EB=B6=80=EC=97=90=20=EB=94=B0=EB=9D=BC=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EC=84=A4=EC=A0=95=20API=EB=A5=BC=20?= =?UTF-8?q?=ED=98=B8=EC=B6=9C=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/home/screen/HomeScreen.kt | 12 +++--------- .../ui/home/screen/HomeViewModel.kt | 19 +++++++++++++++---- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt index 77341064..ce75919f 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt @@ -90,14 +90,10 @@ fun HomeRoute( val showYearMonthPickerState by homeViewModel.showYearMonthPickerState.collectAsStateWithLifecycle() val hasDraft by homeViewModel.hasDraft.collectAsStateWithLifecycle() - // 알림 권한 관련 상태 - val isNotificationPermissionGranted = remember { mutableStateOf(false) } - val requestPermissionLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.RequestPermission(), ) { isGranted: Boolean -> - isNotificationPermissionGranted.value = isGranted - homeViewModel.updateNotificationPermissionGranted(isGranted) + homeViewModel.sendNotification(isGranted) } LaunchedEffect(Unit) { @@ -116,12 +112,10 @@ fun HomeRoute( if (ContextCompat.checkSelfPermission(context, notificationPermission) != PackageManager.PERMISSION_GRANTED) { requestPermissionLauncher.launch(notificationPermission) } else { - isNotificationPermissionGranted.value = true - homeViewModel.updateNotificationPermissionGranted(true) + homeViewModel.sendNotification(true) } } else { - isNotificationPermissionGranted.value = true - homeViewModel.updateNotificationPermissionGranted(true) + homeViewModel.sendNotification(true) } } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt index cdb558d1..6dba95d6 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt @@ -2,6 +2,7 @@ package com.sopt.clody.presentation.ui.home.screen import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.sopt.clody.ClodyFirebaseMessagingService.Companion.getTokenFromPreferences import com.sopt.clody.core.fcm.FcmTokenProvider import com.sopt.clody.core.network.NetworkConnectivityObserver import com.sopt.clody.core.network.NetworkStatus @@ -14,6 +15,7 @@ import com.sopt.clody.domain.repository.DiaryRepository import com.sopt.clody.domain.repository.DraftRepository import com.sopt.clody.domain.repository.NotificationRepository import com.sopt.clody.domain.repository.ReviewRepository +import com.sopt.clody.presentation.ui.auth.timereminder.TimeReminderState 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.ErrorMessageProvider @@ -99,8 +101,6 @@ class HomeViewModel @Inject constructor( private val _hasDraft = MutableStateFlow(false) val hasDraft: StateFlow get() = _hasDraft - private val _isNotificationPermissionGranted = MutableStateFlow(false) - private var isInitialized = false init { @@ -315,7 +315,18 @@ class HomeViewModel @Inject constructor( _showInAppReviewPopup.value = state } - fun updateNotificationPermissionGranted(isGranted: Boolean) { - _isNotificationPermissionGranted.value = isGranted + fun sendNotification(isGranted: Boolean) { + viewModelScope.launch { + val fcmToken = fcmTokenProvider.getToken().orEmpty() + val notificationInfo = getNotificationInfo() ?: return@launch + val requestDto = SendNotificationRequestDto( + isDiaryAlarm = isGranted, + isDraftAlarm = notificationInfo.isDraftAlarm, + isReplyAlarm = isGranted, + time = notificationInfo.time, + fcmToken = fcmToken, + ) + notificationRepository.sendNotification(requestDto) + } } } From 89f56241bb8e2f9615a2a5b2dad0f067c3af0167 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Thu, 14 Aug 2025 21:20:01 +0900 Subject: [PATCH 06/14] [CHORE/#317] ktlintFormat --- .../com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt index 6dba95d6..0adc9dbe 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt @@ -2,7 +2,6 @@ package com.sopt.clody.presentation.ui.home.screen import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.sopt.clody.ClodyFirebaseMessagingService.Companion.getTokenFromPreferences import com.sopt.clody.core.fcm.FcmTokenProvider import com.sopt.clody.core.network.NetworkConnectivityObserver import com.sopt.clody.core.network.NetworkStatus @@ -15,7 +14,6 @@ import com.sopt.clody.domain.repository.DiaryRepository import com.sopt.clody.domain.repository.DraftRepository import com.sopt.clody.domain.repository.NotificationRepository import com.sopt.clody.domain.repository.ReviewRepository -import com.sopt.clody.presentation.ui.auth.timereminder.TimeReminderState 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.ErrorMessageProvider From 53ceff5cb17f1f3c28d7006c33623bc13befb489 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Thu, 14 Aug 2025 22:48:03 +0900 Subject: [PATCH 07/14] =?UTF-8?q?[REFACTOR/#321]=20=EC=A0=90=EA=B2=80=20?= =?UTF-8?q?=EB=8B=A4=EC=9D=B4=EC=96=BC=EB=A1=9C=EA=B7=B8=20string=20?= =?UTF-8?q?=EC=B6=94=EC=B6=9C=EC=9D=84=20=ED=86=B5=ED=95=9C=20=EA=B5=AD?= =?UTF-8?q?=EC=A0=9C=ED=99=94=EB=A5=BC=20=EC=A7=84=ED=96=89=ED=95=A9?= =?UTF-8?q?=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/component/dialog/InspectionDialog.kt | 9 +++++---- app/src/main/res/values-ko/strings.xml | 4 ++++ app/src/main/res/values/strings.xml | 4 ++++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/component/dialog/InspectionDialog.kt b/app/src/main/java/com/sopt/clody/presentation/ui/component/dialog/InspectionDialog.kt index fb885bf5..5b94a2f6 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/component/dialog/InspectionDialog.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/component/dialog/InspectionDialog.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog @@ -63,14 +64,14 @@ fun InspectionDialog( ) Spacer(modifier = Modifier.height(20.dp)) Text( - text = "보다 안정적인 클로디 서비스를 위해\n시스템 점검 중이에요. 곧 다시 만나요!", + text = stringResource(R.string.dialog_inspection_title), color = ClodyTheme.colors.gray03, textAlign = TextAlign.Center, style = ClodyTheme.typography.body3Medium, ) Spacer(modifier = Modifier.height(8.dp)) Text( - text = "점검시간 : $inspectionTime", + text = stringResource(R.string.dialog_inspection_description, inspectionTime), color = ClodyTheme.colors.gray04, textAlign = TextAlign.Center, style = ClodyTheme.typography.body3Medium, @@ -84,7 +85,7 @@ fun InspectionDialog( colors = ButtonDefaults.buttonColors(ClodyTheme.colors.mainYellow), ) { Text( - text = "확인", + text = stringResource(R.string.dialog_inspection_confirm), color = ClodyTheme.colors.gray02, style = ClodyTheme.typography.body3SemiBold, ) @@ -100,7 +101,7 @@ fun InspectionDialog( private fun PreviewInspectionDialog() { BasePreview { InspectionDialog( - inspectionTime = "", + inspectionTime = "Dec 14 (Wed) 12:00 PM ~ Mar 15 (Wed) 10:00 PM", onDismiss = {}, ) } diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 913d128c..b36cb2b7 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -159,6 +159,10 @@ 탈퇴할래요 아니요 + 보다 안정적인 클로디 서비스를 위해\n시스템 점검 중이에요. 곧 다시 만나요! + 점검시간 : %1$s + 확인 + 이어쓰기 알림 설정을 완료했어요. 최대 5개까지 작성할 수 있어요. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5db7f1b3..dbfce628 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -160,6 +160,10 @@ Withdraw Cancel + We\'re conducting system maintenance\nto improve your experience on Clody.\nSee you again soon! + [Maintenance Time]\n%1$s + OK + Continue writing reminders are now on! Field required to send. From b6c5e3904c80a877d3187c3d9e8cbc4d3790e762 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Thu, 14 Aug 2025 22:48:50 +0900 Subject: [PATCH 08/14] =?UTF-8?q?[REFACTOR/#321]=20=EC=A0=90=EA=B2=80=20?= =?UTF-8?q?=EC=8B=9C=EC=9E=91=EC=8B=9C=EA=B0=84/=EC=A2=85=EB=A3=8C?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=EC=9D=84=20"2025-08-11T18:00:00"=20=ED=98=95?= =?UTF-8?q?=ED=83=9C=20=EA=B7=B8=EB=8C=80=EB=A1=9C=20=EB=B0=98=ED=99=98?= =?UTF-8?q?=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../remote/appupdate/AppUpdateCheckerImpl.kt | 19 ++++--------------- .../domain/appupdate/AppUpdateChecker.kt | 2 +- 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/data/remote/appupdate/AppUpdateCheckerImpl.kt b/app/src/main/java/com/sopt/clody/data/remote/appupdate/AppUpdateCheckerImpl.kt index d41f2a0b..eb71132e 100644 --- a/app/src/main/java/com/sopt/clody/data/remote/appupdate/AppUpdateCheckerImpl.kt +++ b/app/src/main/java/com/sopt/clody/data/remote/appupdate/AppUpdateCheckerImpl.kt @@ -5,8 +5,6 @@ import com.sopt.clody.domain.appupdate.AppUpdateChecker import com.sopt.clody.domain.model.AppUpdateState import com.sopt.clody.domain.util.VersionComparator import java.time.LocalDateTime -import java.time.format.TextStyle -import java.util.Locale import javax.inject.Inject class AppUpdateCheckerImpl @Inject constructor( @@ -41,22 +39,13 @@ class AppUpdateCheckerImpl @Inject constructor( return now.isAfter(start) && now.isBefore(end) } - override fun getInspectionTimeText(): String? { + override suspend fun getInspectionTimeText(): Pair? { val start = remoteConfigDataSource.getInspectionStart() val end = remoteConfigDataSource.getInspectionEnd() if (start == null || end == null) return null - val startText = formatDateTimeWithDayOfWeek(start) - val endText = formatDateTimeWithDayOfWeek(end) - return "$startText ~ $endText" - } - - private fun formatDateTimeWithDayOfWeek(dateTime: LocalDateTime): String { - val dayOfWeek = dateTime.dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.KOREAN) - val month = dateTime.monthValue - val day = dateTime.dayOfMonth - val hour = dateTime.hour.toString().padStart(2, '0') - - return "$month/$day($dayOfWeek) ${hour}시" + val startText = start.toString() + val endText = end.toString() + return startText to endText } } diff --git a/app/src/main/java/com/sopt/clody/domain/appupdate/AppUpdateChecker.kt b/app/src/main/java/com/sopt/clody/domain/appupdate/AppUpdateChecker.kt index de3f683f..2d2c25d9 100644 --- a/app/src/main/java/com/sopt/clody/domain/appupdate/AppUpdateChecker.kt +++ b/app/src/main/java/com/sopt/clody/domain/appupdate/AppUpdateChecker.kt @@ -5,5 +5,5 @@ import com.sopt.clody.domain.model.AppUpdateState interface AppUpdateChecker { suspend fun getAppUpdateState(currentVersion: String): AppUpdateState suspend fun isUnderInspection(): Boolean - fun getInspectionTimeText(): String? + suspend fun getInspectionTimeText(): Pair? } From 138c5baddf881fb4ec44050601bdc3ec7b955143 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Thu, 14 Aug 2025 22:49:24 +0900 Subject: [PATCH 09/14] =?UTF-8?q?[REFACTOR/#321]=20=EC=96=B8=EC=96=B4?= =?UTF-8?q?=EC=97=90=20=EB=A7=9E=EB=8A=94=20=EC=A0=90=EA=B2=80=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=ED=8F=AC=EB=A7=B7=EC=9D=84=20=EC=A0=9C=EA=B3=B5?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=ED=95=A8=EC=88=98=EB=A5=BC=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../utils/language/LanguageProvider.kt | 1 + .../utils/language/LanguageProviderImpl.kt | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProvider.kt b/app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProvider.kt index 57a7889a..1f3fc771 100644 --- a/app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProvider.kt +++ b/app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProvider.kt @@ -5,6 +5,7 @@ import com.sopt.clody.presentation.ui.setting.screen.SettingOptionUrls interface LanguageProvider { fun getCurrentLanguageTag(): String + fun getInspectionTimeText(start: String, end: String): String? fun getLoginType(): OAuthProvider fun getNicknameMaxLength(): Int fun getDiaryMaxLength(): Int diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProviderImpl.kt b/app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProviderImpl.kt index 933ecc97..d1a8410b 100644 --- a/app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProviderImpl.kt +++ b/app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProviderImpl.kt @@ -2,6 +2,8 @@ package com.sopt.clody.presentation.utils.language import com.sopt.clody.data.datastore.OAuthProvider import com.sopt.clody.presentation.ui.setting.screen.SettingOptionUrls +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter import java.util.Locale import javax.inject.Inject @@ -13,6 +15,25 @@ class LanguageProviderImpl @Inject constructor() : LanguageProvider { override fun getCurrentLanguageTag(): String = locale.toLanguageTag() // e.g., "ko-KR" or "en-US" + override fun getInspectionTimeText(start: String, end: String): String? { + return runCatching { + val startLdt = LocalDateTime.parse(start, DateTimeFormatter.ISO_LOCAL_DATE_TIME) + val endLdt = LocalDateTime.parse(end, DateTimeFormatter.ISO_LOCAL_DATE_TIME) + + if (isKorean()) { + val koPattern = DateTimeFormatter.ofPattern("M/d(E) HH시mm분", Locale.KOREAN) + "${startLdt.format(koPattern)} ~ ${endLdt.format(koPattern)}" + } else { + val enDatePattern = DateTimeFormatter.ofPattern("MMM d (EEE)", Locale.ENGLISH) + val enTimePattern = DateTimeFormatter.ofPattern("h:mm a", Locale.ENGLISH) + + val left = "${startLdt.format(enDatePattern)}, ${startLdt.format(enTimePattern)}" + val right = "${endLdt.format(enDatePattern)} ${endLdt.format(enTimePattern)}" + "$left ~ $right" + } + }.getOrNull() + } + override fun getLoginType(): OAuthProvider = if (isKorean()) OAuthProvider.KAKAO else OAuthProvider.GOOGLE From 2a0c8e819ad3edaedc45b68bf57b76834fb7be22 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Thu, 14 Aug 2025 22:49:40 +0900 Subject: [PATCH 10/14] =?UTF-8?q?[REFACTOR/#321]=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=EC=82=AC=ED=95=AD=EC=9D=84=20=EC=A0=81=EC=9A=A9=ED=95=A9?= =?UTF-8?q?=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sopt/clody/presentation/ui/splash/SplashViewModel.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashViewModel.kt index db366f59..1b302dc3 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashViewModel.kt @@ -10,6 +10,7 @@ import com.sopt.clody.domain.model.AppUpdateState import com.sopt.clody.domain.repository.TokenRepository import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils +import com.sopt.clody.presentation.utils.language.LanguageProvider import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -25,6 +26,7 @@ class SplashViewModel @AssistedInject constructor( @Assisted initialState: SplashContract.SplashState, private val tokenRepository: TokenRepository, private val appUpdateChecker: AppUpdateChecker, + private val languageProvider: LanguageProvider, ) : MavericksViewModel(initialState) { private val _intents = Channel(BUFFERED) @@ -64,7 +66,10 @@ class SplashViewModel @AssistedInject constructor( private suspend fun checkInspectionAndHandle(): Boolean { if (appUpdateChecker.isUnderInspection()) { - val inspectionText = appUpdateChecker.getInspectionTimeText() + val inspectionTextRaw = appUpdateChecker.getInspectionTimeText() + val inspectionText = inspectionTextRaw?.let { (start, end) -> + languageProvider.getInspectionTimeText(start, end) + } setState { copy( showInspectionDialog = true, From d9026f8594b9aae3db65f09bee11dd2e47aeb6f1 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Fri, 15 Aug 2025 00:23:36 +0900 Subject: [PATCH 11/14] =?UTF-8?q?[REFACTOR/#322]=20TimePicker=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=EC=9D=84=20=EC=88=98=EC=A0=95=ED=95=A9=EB=8B=88?= =?UTF-8?q?=EB=8B=A4.=20"=EC=98=A4=EC=A0=84"=EC=9C=BC=EB=A1=9C=20=ED=95=98?= =?UTF-8?q?=EB=93=9C=EC=BD=94=EB=94=A9=20=EB=90=98=EC=96=B4=EC=9E=88?= =?UTF-8?q?=EC=96=B4=EC=84=9C=20TimePeriod=EA=B0=80=20=EB=B0=94=EB=80=8C?= =?UTF-8?q?=EC=A7=80=EC=95=8A=EC=95=98=EC=8A=B5=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NotificationSettingTimePicker.kt | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationSettingTimePicker.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationSettingTimePicker.kt index 23c3ecde..e6fae3c2 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationSettingTimePicker.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationSettingTimePicker.kt @@ -36,14 +36,21 @@ fun NotificationSettingTimePicker( onDismissRequest: () -> Unit, onConfirm: (TimePeriod, String, String) -> Unit, ) { - val amPmItemsLabel = TimePeriod.entries.map { it.getLabel() } - val amPmItems = remember { amPmItemsLabel } + val amPmEnumItems = listOf(TimePeriod.AM, TimePeriod.PM) + val amPmLabelItems = amPmEnumItems.map { it.getLabel() } + val hourItems = remember { (1..12).map { it.toString() } } val minuteItems = remember { listOf("00", "10", "20", "30", "40", "50") } - val amPmPickerState = rememberPickerState() - val hourPickerState = rememberPickerState() - val minutePickerState = rememberPickerState() + val amPmPickerState = rememberPickerState().apply { + selectedItem = amPmLabelItems[1] + } + val hourPickerState = rememberPickerState().apply { + selectedItem = "9" + } + val minutePickerState = rememberPickerState().apply { + selectedItem = "30" + } Surface( modifier = Modifier @@ -105,7 +112,7 @@ fun NotificationSettingTimePicker( Spacer(Modifier.weight(1f)) ClodyPicker( state = amPmPickerState, - items = amPmItems, + items = amPmLabelItems, startIndex = 1, visibleItemsCount = 3, infiniteScroll = false, @@ -138,8 +145,12 @@ fun NotificationSettingTimePicker( } ClodyButton( onClick = { - val selectedPeriod = if (amPmPickerState.selectedItem == "오전") TimePeriod.AM else TimePeriod.PM - onConfirm(selectedPeriod, hourPickerState.selectedItem, minutePickerState.selectedItem) + val selectedLabel = amPmPickerState.selectedItem + val selectedPeriod = amPmEnumItems.getOrElse(amPmLabelItems.indexOf(selectedLabel)) { TimePeriod.PM } + val selectedHour = hourPickerState.selectedItem + val selectedMinute = minutePickerState.selectedItem + + onConfirm(selectedPeriod, selectedHour, selectedMinute) }, text = stringResource(R.string.bottom_sheet_notification_time_change_confirm), enabled = true, From 3b59e195c1f5630b0c8a4dd3b21e620c253cafad Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Fri, 15 Aug 2025 12:10:06 +0900 Subject: [PATCH 12/14] =?UTF-8?q?[REFACTOR/#321]=20=EC=A0=90=EA=B2=80?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=EC=9D=84=20=EC=9C=A0=EC=A0=80=EC=9D=98=20?= =?UTF-8?q?=ED=83=80=EC=9E=84=EC=A1=B4=EC=97=90=20=EB=A7=9E=EC=B6=B0=20?= =?UTF-8?q?=EB=B3=80=ED=99=98=ED=95=9C=20=ED=9B=84=20=EB=B3=B4=EC=97=AC?= =?UTF-8?q?=EC=A4=84=20=EC=88=98=20=EC=9E=88=EB=8F=84=EB=A1=9D=20=ED=95=A9?= =?UTF-8?q?=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../remote/appupdate/AppUpdateCheckerImpl.kt | 22 ++++++++++--------- .../utils/language/LanguageProviderImpl.kt | 14 ++++++++---- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/data/remote/appupdate/AppUpdateCheckerImpl.kt b/app/src/main/java/com/sopt/clody/data/remote/appupdate/AppUpdateCheckerImpl.kt index eb71132e..c7ab9af5 100644 --- a/app/src/main/java/com/sopt/clody/data/remote/appupdate/AppUpdateCheckerImpl.kt +++ b/app/src/main/java/com/sopt/clody/data/remote/appupdate/AppUpdateCheckerImpl.kt @@ -4,7 +4,8 @@ import com.sopt.clody.data.remote.datasource.RemoteConfigDataSource import com.sopt.clody.domain.appupdate.AppUpdateChecker import com.sopt.clody.domain.model.AppUpdateState import com.sopt.clody.domain.util.VersionComparator -import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZonedDateTime import javax.inject.Inject class AppUpdateCheckerImpl @Inject constructor( @@ -35,17 +36,18 @@ class AppUpdateCheckerImpl @Inject constructor( override suspend fun isUnderInspection(): Boolean { val start = remoteConfigDataSource.getInspectionStart() ?: return false val end = remoteConfigDataSource.getInspectionEnd() ?: return false - val now = LocalDateTime.now() - return now.isAfter(start) && now.isBefore(end) + val serverZone = ZoneId.of("Asia/Seoul") + + val nowServer = ZonedDateTime.now(serverZone) + val startZ = start.atZone(serverZone) + val endZ = end.atZone(serverZone) + + return nowServer.isAfter(startZ) && nowServer.isBefore(endZ) } override suspend fun getInspectionTimeText(): Pair? { - val start = remoteConfigDataSource.getInspectionStart() - val end = remoteConfigDataSource.getInspectionEnd() - if (start == null || end == null) return null - - val startText = start.toString() - val endText = end.toString() - return startText to endText + val start = remoteConfigDataSource.getInspectionStart() ?: return null + val end = remoteConfigDataSource.getInspectionEnd() ?: return null + return start.toString() to end.toString() } } diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProviderImpl.kt b/app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProviderImpl.kt index d1a8410b..0f8b3705 100644 --- a/app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProviderImpl.kt +++ b/app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProviderImpl.kt @@ -3,6 +3,7 @@ package com.sopt.clody.presentation.utils.language import com.sopt.clody.data.datastore.OAuthProvider import com.sopt.clody.presentation.ui.setting.screen.SettingOptionUrls import java.time.LocalDateTime +import java.time.ZoneId import java.time.format.DateTimeFormatter import java.util.Locale import javax.inject.Inject @@ -20,15 +21,20 @@ class LanguageProviderImpl @Inject constructor() : LanguageProvider { val startLdt = LocalDateTime.parse(start, DateTimeFormatter.ISO_LOCAL_DATE_TIME) val endLdt = LocalDateTime.parse(end, DateTimeFormatter.ISO_LOCAL_DATE_TIME) + val serverZone = ZoneId.of("Asia/Seoul") + val userZone = ZoneId.systemDefault() + + val startUser = startLdt.atZone(serverZone).withZoneSameInstant(userZone) + val endUser = endLdt.atZone(serverZone).withZoneSameInstant(userZone) + if (isKorean()) { val koPattern = DateTimeFormatter.ofPattern("M/d(E) HH시mm분", Locale.KOREAN) - "${startLdt.format(koPattern)} ~ ${endLdt.format(koPattern)}" + "${startUser.format(koPattern)} ~ ${endUser.format(koPattern)}" } else { val enDatePattern = DateTimeFormatter.ofPattern("MMM d (EEE)", Locale.ENGLISH) val enTimePattern = DateTimeFormatter.ofPattern("h:mm a", Locale.ENGLISH) - - val left = "${startLdt.format(enDatePattern)}, ${startLdt.format(enTimePattern)}" - val right = "${endLdt.format(enDatePattern)} ${endLdt.format(enTimePattern)}" + val left = "${startUser.format(enDatePattern)}, ${startUser.format(enTimePattern)}" + val right = "${endUser.format(enDatePattern)} ${endUser.format(enTimePattern)}" "$left ~ $right" } }.getOrNull() From 7194bfd11f733ca15fdd952d3b3d2a3a85db15ee Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Fri, 15 Aug 2025 16:37:33 +0900 Subject: [PATCH 13/14] =?UTF-8?q?[CHORE/#321]=20=EC=98=81=EC=96=B4?= =?UTF-8?q?=EB=B2=84=EC=A0=84=EB=8F=84=2024=EC=8B=9C=EA=B0=84=20=ED=98=95?= =?UTF-8?q?=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=ED=86=B5=EC=9D=BC=ED=95=A9?= =?UTF-8?q?=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clody/presentation/utils/language/LanguageProviderImpl.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProviderImpl.kt b/app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProviderImpl.kt index 0f8b3705..8363220b 100644 --- a/app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProviderImpl.kt +++ b/app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProviderImpl.kt @@ -32,7 +32,7 @@ class LanguageProviderImpl @Inject constructor() : LanguageProvider { "${startUser.format(koPattern)} ~ ${endUser.format(koPattern)}" } else { val enDatePattern = DateTimeFormatter.ofPattern("MMM d (EEE)", Locale.ENGLISH) - val enTimePattern = DateTimeFormatter.ofPattern("h:mm a", Locale.ENGLISH) + val enTimePattern = DateTimeFormatter.ofPattern("HH:mm", Locale.ENGLISH) val left = "${startUser.format(enDatePattern)}, ${startUser.format(enTimePattern)}" val right = "${endUser.format(enDatePattern)} ${endUser.format(enTimePattern)}" "$left ~ $right" From 8581b98915bedfc70c814851710e7da2a93d8c56 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Tue, 26 Aug 2025 09:30:55 +0900 Subject: [PATCH 14/14] =?UTF-8?q?[CHORE/#321]=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EC=82=AC=ED=95=AD=EC=9D=84=20=EB=B0=98=EC=98=81=ED=95=A9?= =?UTF-8?q?=EB=8B=88=EB=8B=A4.=20-=20isUnderInspection=20=ED=95=A8?= =?UTF-8?q?=EC=88=98=EC=97=90=EC=84=9C=20=EC=A0=90=EA=B2=80=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=EA=B2=80=EC=82=AC=EB=A5=BC=20=EC=B4=88=EA=B3=BC/?= =?UTF-8?q?=EB=AF=B8=EB=A7=8C=EC=97=90=EC=84=9C=20=EC=9D=B4=EC=83=81/?= =?UTF-8?q?=EC=9D=B4=ED=95=98=EB=A1=9C=20=EA=B0=9C=EC=84=A0=20+=20companio?= =?UTF-8?q?n=20object=20=EB=B6=84=EB=A6=AC=20-=20getInspectionTimeText=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=EC=97=90=EC=84=9C=20=EB=8B=A8=EC=9D=BC=20?= =?UTF-8?q?=EC=B1=85=EC=9E=84=20=EC=9B=90=EC=B9=99=EC=97=90=20=EB=94=B0?= =?UTF-8?q?=EB=9D=BC=20=ED=8F=AC=EB=A7=B7=ED=8C=85=20=ED=95=A8=EC=88=98=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=20+=20companion=20object=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../remote/appupdate/AppUpdateCheckerImpl.kt | 18 ++++++- .../utils/language/LanguageProviderImpl.kt | 50 +++++++++++-------- 2 files changed, 44 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/data/remote/appupdate/AppUpdateCheckerImpl.kt b/app/src/main/java/com/sopt/clody/data/remote/appupdate/AppUpdateCheckerImpl.kt index c7ab9af5..987fd7f1 100644 --- a/app/src/main/java/com/sopt/clody/data/remote/appupdate/AppUpdateCheckerImpl.kt +++ b/app/src/main/java/com/sopt/clody/data/remote/appupdate/AppUpdateCheckerImpl.kt @@ -33,21 +33,35 @@ class AppUpdateCheckerImpl @Inject constructor( } } + /** + * Firebase RemoteConfig 로부터 점검 시간에 해당 하는지 검사하는 함수. + * + * @return 점검 시간 해당 여부 + * */ override suspend fun isUnderInspection(): Boolean { val start = remoteConfigDataSource.getInspectionStart() ?: return false val end = remoteConfigDataSource.getInspectionEnd() ?: return false - val serverZone = ZoneId.of("Asia/Seoul") + val serverZone = ZoneId.of(SERVER_TIMEZONE) val nowServer = ZonedDateTime.now(serverZone) val startZ = start.atZone(serverZone) val endZ = end.atZone(serverZone) - return nowServer.isAfter(startZ) && nowServer.isBefore(endZ) + return nowServer in startZ..endZ } + /** + * Firebase RemoteConfig 로부터 점검 시간을 가져와 반환하는 함수. + * + * @return 점검 시작 시간과 종료 시간을 "2025-08-11T18:00:00" 형식으로 반환 + * */ override suspend fun getInspectionTimeText(): Pair? { val start = remoteConfigDataSource.getInspectionStart() ?: return null val end = remoteConfigDataSource.getInspectionEnd() ?: return null return start.toString() to end.toString() } + + companion object { + private const val SERVER_TIMEZONE = "Asia/Seoul" + } } diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProviderImpl.kt b/app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProviderImpl.kt index 8363220b..5abea87b 100644 --- a/app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProviderImpl.kt +++ b/app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProviderImpl.kt @@ -4,6 +4,7 @@ import com.sopt.clody.data.datastore.OAuthProvider import com.sopt.clody.presentation.ui.setting.screen.SettingOptionUrls import java.time.LocalDateTime import java.time.ZoneId +import java.time.ZonedDateTime import java.time.format.DateTimeFormatter import java.util.Locale import javax.inject.Inject @@ -18,28 +19,29 @@ class LanguageProviderImpl @Inject constructor() : LanguageProvider { override fun getInspectionTimeText(start: String, end: String): String? { return runCatching { - val startLdt = LocalDateTime.parse(start, DateTimeFormatter.ISO_LOCAL_DATE_TIME) - val endLdt = LocalDateTime.parse(end, DateTimeFormatter.ISO_LOCAL_DATE_TIME) - - val serverZone = ZoneId.of("Asia/Seoul") + val serverZone = ZoneId.of(SERVER_TIMEZONE) val userZone = ZoneId.systemDefault() - val startUser = startLdt.atZone(serverZone).withZoneSameInstant(userZone) - val endUser = endLdt.atZone(serverZone).withZoneSameInstant(userZone) - - if (isKorean()) { - val koPattern = DateTimeFormatter.ofPattern("M/d(E) HH시mm분", Locale.KOREAN) - "${startUser.format(koPattern)} ~ ${endUser.format(koPattern)}" - } else { - val enDatePattern = DateTimeFormatter.ofPattern("MMM d (EEE)", Locale.ENGLISH) - val enTimePattern = DateTimeFormatter.ofPattern("HH:mm", Locale.ENGLISH) - val left = "${startUser.format(enDatePattern)}, ${startUser.format(enTimePattern)}" - val right = "${endUser.format(enDatePattern)} ${endUser.format(enTimePattern)}" - "$left ~ $right" - } + val startUser = LocalDateTime.parse(start).atZone(serverZone).withZoneSameInstant(userZone) + val endUser = LocalDateTime.parse(end).atZone(serverZone).withZoneSameInstant(userZone) + + formatInspectionTime(startUser, endUser) }.getOrNull() } + private fun formatInspectionTime(startUser: ZonedDateTime, endUser: ZonedDateTime): String { + return if (isKorean()) { + val koPattern = DateTimeFormatter.ofPattern(INSPECTION_TIME_FORMAT_KO, Locale.KOREAN) + "${startUser.format(koPattern)} ~ ${endUser.format(koPattern)}" + } else { + val enDateFormatter = DateTimeFormatter.ofPattern(INSPECTION_DATE_FORMAT_EN, Locale.ENGLISH) + val enTimeFormatter = DateTimeFormatter.ofPattern(INSPECTION_TIME_FORMAT_EN, Locale.ENGLISH) + val left = "${startUser.format(enDateFormatter)}, ${startUser.format(enTimeFormatter)}" + val right = "${endUser.format(enDateFormatter)} ${endUser.format(enTimeFormatter)}" + "$left ~ $right" + } + } + override fun getLoginType(): OAuthProvider = if (isKorean()) OAuthProvider.KAKAO else OAuthProvider.GOOGLE @@ -53,10 +55,14 @@ class LanguageProviderImpl @Inject constructor() : LanguageProvider { if (isKorean()) option.koUrl else option.enUrl companion object { - const val LANGUAGE_KO = "ko" - const val NICKNAME_MAX_LENGTH_EN = 15 - const val NICKNAME_MAX_LENGTH_KO = 10 - const val DIARY_MAX_LENGTH_EN = 100 - const val DIARY_MAX_LENGTH_KO = 50 + private const val LANGUAGE_KO = "ko" + private const val SERVER_TIMEZONE = "Asia/Seoul" + private const val INSPECTION_TIME_FORMAT_KO = "M/d(E) HH시mm분" + private const val INSPECTION_DATE_FORMAT_EN = "MMM d (EEE)" + private const val INSPECTION_TIME_FORMAT_EN = "HH:mm" + private const val NICKNAME_MAX_LENGTH_EN = 15 + private const val NICKNAME_MAX_LENGTH_KO = 10 + private const val DIARY_MAX_LENGTH_EN = 100 + private const val DIARY_MAX_LENGTH_KO = 50 } }