Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
710a198
[REFACTOR/#317] 알림 권한 로직 개선
MoonsuKang Aug 11, 2025
7a956e3
[REFACTOR/#319] 홈 화면 데이터 로딩 로직 개선 및 동시성 관리
MoonsuKang Aug 11, 2025
dbfb291
[MOD/#317] 버전정보 수정
SYAAINN Aug 14, 2025
fdd6da5
[CHORE/#317] 불필요 파라미터 삭ㅈ
SYAAINN Aug 14, 2025
2dd1d8f
[REFACTOR/#317] 미사용 변수 삭제 후 홈화면에서 알림 권한 요청 승인 여부에 따라 알림 설정 API를 호출하도록…
SYAAINN Aug 14, 2025
89f5624
[CHORE/#317] ktlintFormat
SYAAINN Aug 14, 2025
f6584eb
Merge pull request #320 from Team-Clody/refactor/#319-upgrade-focus
SYAAINN Aug 14, 2025
53ceff5
[REFACTOR/#321] 점검 다이얼로그 string 추출을 통한 국제화를 진행합니다.
SYAAINN Aug 14, 2025
b6c5e39
[REFACTOR/#321] 점검 시작시간/종료시간을 "2025-08-11T18:00:00" 형태 그대로 반환합니다.
SYAAINN Aug 14, 2025
138c5ba
[REFACTOR/#321] 언어에 맞는 점검시간 포맷을 제공하는 함수를 구현합니다.
SYAAINN Aug 14, 2025
2a0c8e8
[REFACTOR/#321] 변경사항을 적용합니다.
SYAAINN Aug 14, 2025
d9026f8
[REFACTOR/#322] TimePicker 로직을 수정합니다. "오전"으로 하드코딩 되어있어서 TimePeriod가 바…
SYAAINN Aug 14, 2025
3b59e19
[REFACTOR/#321] 점검시간을 유저의 타임존에 맞춰 변환한 후 보여줄 수 있도록 합니다.
SYAAINN Aug 15, 2025
7194bfd
[CHORE/#321] 영어버전도 24시간 형식으로 통일합니다.
SYAAINN Aug 15, 2025
8581b98
[CHORE/#321] 리뷰 사항을 반영합니다.
SYAAINN Aug 26, 2025
c70281e
Merge pull request #324 from Team-Clody/feat/#321-inspection-i18n
SYAAINN Aug 26, 2025
c4d510e
Merge pull request #325 from Team-Clody/refactor/#322-notification-ti…
SYAAINN Aug 26, 2025
647eeed
Merge pull request #318 from Team-Clody/refactor/#317-notification-re…
SYAAINN Aug 26, 2025
db3509a
[MOD] VersionCode, VersionName 수정
SYAAINN Aug 26, 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
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ android {
applicationId = "com.sopt.clody"
minSdk = 28
targetSdk = 35
versionCode = 28
versionName = "1.4.0"
versionCode = 30
versionName = "1.5.1"
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", "")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +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.format.TextStyle
import java.util.Locale
import java.time.ZoneId
import java.time.ZonedDateTime
import javax.inject.Inject

class AppUpdateCheckerImpl @Inject constructor(
Expand Down Expand Up @@ -34,29 +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 now = LocalDateTime.now()
return now.isAfter(start) && now.isBefore(end)
}
val serverZone = ZoneId.of(SERVER_TIMEZONE)

override fun getInspectionTimeText(): String? {
val start = remoteConfigDataSource.getInspectionStart()
val end = remoteConfigDataSource.getInspectionEnd()
if (start == null || end == null) return null
val nowServer = ZonedDateTime.now(serverZone)
val startZ = start.atZone(serverZone)
val endZ = end.atZone(serverZone)

val startText = formatDateTimeWithDayOfWeek(start)
val endText = formatDateTimeWithDayOfWeek(end)
return "$startText ~ $endText"
return nowServer in startZ..endZ
}

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')
/**
* Firebase RemoteConfig 로부터 점검 시간을 가져와 반환하는 함수.
*
* @return 점검 시작 시간과 종료 시간을 "2025-08-11T18:00:00" 형식으로 반환
* */
override suspend fun getInspectionTimeText(): Pair<String, String>? {
val start = remoteConfigDataSource.getInspectionStart() ?: return null
val end = remoteConfigDataSource.getInspectionEnd() ?: return null
return start.toString() to end.toString()
}

return "$month/$day($dayOfWeek) ${hour}시"
companion object {
private const val SERVER_TIMEZONE = "Asia/Seoul"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, String>?
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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) {
Expand All @@ -103,14 +76,14 @@ fun TimeReminderRoute(
TimeReminderScreen(
onStartClick = {
viewModel.setSelectedTime(TimePeriod.PM, "9", "30")
viewModel.sendNotification(context, isNotificationPermissionGranted.value)
viewModel.sendNotification(context)
},
onTimeSelected = { period, hour, minute ->
viewModel.setSelectedTime(period, hour, minute)
},
onCompleteClick = {
AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.ONBOARDING_ALARM)
viewModel.sendNotification(context, isNotificationPermissionGranted.value)
viewModel.sendNotification(context)
},
isLoading = timeReminderState is TimeReminderState.Loading,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -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,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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,
)
Expand All @@ -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 = {},
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -40,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
Expand All @@ -49,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
Expand Down Expand Up @@ -84,6 +87,12 @@ fun HomeRoute(
val showYearMonthPickerState by homeViewModel.showYearMonthPickerState.collectAsStateWithLifecycle()
val hasDraft by homeViewModel.hasDraft.collectAsStateWithLifecycle()

val requestPermissionLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestPermission(),
) { isGranted: Boolean ->
homeViewModel.sendNotification(isGranted)
}

LaunchedEffect(Unit) {
AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.HOME)

Expand All @@ -93,32 +102,33 @@ 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()
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 {
homeViewModel.sendNotification(true)
}
} catch (e: Exception) {
homeViewModel.setErrorState(true, "데이터를 불러오는데 실패했습니다.")
} else {
homeViewModel.sendNotification(true)
}
}

LaunchedEffect(Unit) {
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,
Expand Down Expand Up @@ -273,10 +283,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)
},
Expand Down Expand Up @@ -320,8 +331,7 @@ fun HomeScreen(
FailureScreen(
message = errorMessage,
confirmAction = {
homeViewModel.refreshCalendarDataCalendarData(selectedYear, selectedMonth)
homeViewModel.loadDailyDiariesData(selectedYear, selectedMonth, selectedDate.dayOfMonth)
homeViewModel.updateYearMonthAndLoadData(selectedYear, selectedMonth, selectedDate.dayOfMonth)
},
)
} else {
Expand Down Expand Up @@ -388,8 +398,7 @@ fun HomeScreen(
LoadingScreen()
}

is DeleteDiaryState.Success -> {
}
is DeleteDiaryState.Success -> {}

is DeleteDiaryState.Failure -> {
homeViewModel.setErrorState(true, stringResource(R.string.home_error_delete_diary))
Expand Down Expand Up @@ -433,8 +442,7 @@ fun HomeScreen(
selectedYear = selectedYear,
selectedMonth = selectedMonth,
onYearMonthSelected = { year, month ->
homeViewModel.updateSelectedDiaryDate(DiaryDateData(year, month))
homeViewModel.loadCalendarData(year, month)
homeViewModel.updateYearMonthAndLoadData(year, month)
},
)
}
Expand Down
Loading