Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ fun TimeReminderRoute(

TimeReminderScreen(
onStartClick = {
viewModel.setFixedTime(TimePeriod.PM, "9", "30")
viewModel.setSelectedTime(TimePeriod.PM, "9", "30")
viewModel.sendNotification(context, isNotificationPermissionGranted.value)
},
onTimeSelected = { period, hour, minute ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ import com.sopt.clody.data.remote.dto.request.SendNotificationRequestDto
import com.sopt.clody.data.remote.util.NetworkUtil
import com.sopt.clody.domain.repository.NotificationRepository
import com.sopt.clody.presentation.utils.extension.TimePeriod
import com.sopt.clody.presentation.utils.extension.convertUTZtoKST
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify the function name accuracy.

The import convertUTZtoKST suggests converting from "UTZ" (possibly User Time Zone) to KST, but this naming could be confusing. Ensure the function name accurately reflects the conversion direction and consider using more descriptive names like convertUserTimeToKST.


🏁 Script executed:

#!/bin/bash
# Description: Verify the actual implementation and naming of the timezone conversion function
# Expected: Find the function definition to understand the conversion direction

ast-grep --pattern 'fun convertUTZtoKST($$$) {
  $$$
}'

Length of output: 57


🏁 Script executed:

#!/bin/bash
# Search for the definition and usages of convertUTZtoKST to verify its direction and naming
rg -n "convertUTZtoKST" -C 3 .

Length of output: 5295


Rename convertUTZtoKST for clarity

The convertUTZtoKST utility indeed reads from the system default (user) timezone and converts that time to KST. However, “UTZ” is non-standard and can be easily confused with “UTC.” Consider renaming this function to more clearly express its behavior, for example:

  • convertLocalTimeToKST
  • convertUserTimeToKST

Locations to update:

  • app/src/main/java/com/sopt/clody/presentation/utils/extension/TimeZoneExt.kt (line 39)
  • All call sites in NotificationSettingViewModel.kt and TimeReminderViewModel.kt
🤖 Prompt for AI Agents
In app/src/main/java/com/sopt/clody/presentation/utils/extension/TimeZoneExt.kt
around line 39, rename the function convertUTZtoKST to a clearer name such as
convertLocalTimeToKST or convertUserTimeToKST to better reflect that it converts
from the system default timezone to KST. Then update all call sites of this
function in NotificationSettingViewModel.kt and TimeReminderViewModel.kt
accordingly to use the new function name.

import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_NETWORK_MESSAGE
import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_TEMPORARY_MESSAGE
import com.sopt.clody.presentation.utils.network.ErrorMessages.UNKNOWN_ERROR
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import java.util.Locale
import javax.inject.Inject

@HiltViewModel
Expand Down Expand Up @@ -80,18 +80,6 @@ class TimeReminderViewModel @Inject constructor(
}

fun setSelectedTime(period: TimePeriod, hour: String, minute: String) {
selectedTime = formatTime(period, hour, minute)
}

fun setFixedTime(period: TimePeriod, hour: String, minute: String) {
selectedTime = formatTime(period, hour, minute)
}

private fun formatTime(period: TimePeriod, hour: String, minute: String): String {
val hourInt = when (period) {
TimePeriod.PM -> if (hour.toInt() != 12) hour.toInt() + 12 else 12
TimePeriod.AM -> if (hour.toInt() == 12) 0 else hour.toInt()
}
return String.format(Locale.US, "%02d:%02d", hourInt, minute.toInt())
selectedTime = convertUTZtoKST(timePeriod = period, hour = hour, minute = minute)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import java.time.LocalDate
import java.time.ZoneId
import javax.inject.Inject

@HiltViewModel
Expand Down Expand Up @@ -232,9 +233,14 @@ class HomeViewModel @Inject constructor(
}

fun canWriteDiary(): Boolean {
val userTimeZone = ZoneId.systemDefault().id
val today = LocalDate.now()
val selected = _selectedDate.value
val isAvailableDay = selected == today || selected == today.minusDays(1)
val isAvailableDay = if (userTimeZone == "Asia/Seoul") {
selected == today || selected == today.minusDays(1)
} else {
selected == today
}
Comment on lines +239 to +243
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.

깜빡하고 이 부분도 해당 커밋에 합쳐버렸는데, 전날 일기 작성 불가능하게 분기처리 해놓은 부분입니다!

return _diaryCount.value == 0 && isAvailableDay
}

Expand Down Expand Up @@ -276,7 +282,7 @@ class HomeViewModel @Inject constructor(
isDiaryAlarm = info.isDiaryAlarm,
isDraftAlarm = true,
isReplyAlarm = info.isReplyAlarm,
time = info.time.ifEmpty { "21:30" },
time = info.time,
fcmToken = fcmToken,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ import com.sopt.clody.R
import com.sopt.clody.presentation.ui.component.button.ClodyButton
import com.sopt.clody.presentation.ui.component.timepicker.ClodyPicker
import com.sopt.clody.presentation.ui.component.timepicker.rememberPickerState
import com.sopt.clody.presentation.utils.extension.to24HourFormat
import com.sopt.clody.presentation.utils.extension.TimePeriod
import com.sopt.clody.ui.theme.ClodyTheme

@Composable
fun NotificationSettingTimePicker(
onDismissRequest: () -> Unit,
onConfirm: (String) -> Unit,
onConfirm: (TimePeriod, String, String) -> Unit,
) {
val amPmItems = remember { listOf("오전", "오후") }
val hourItems = remember { (1..12).map { it.toString() } }
Expand Down Expand Up @@ -137,12 +137,8 @@ fun NotificationSettingTimePicker(
}
ClodyButton(
onClick = {
val selectedTime = Triple(
amPmPickerState.selectedItem,
hourPickerState.selectedItem,
minutePickerState.selectedItem,
).to24HourFormat()
onConfirm(selectedTime)
val selectedPeriod = if (amPmPickerState.selectedItem == "오전") TimePeriod.AM else TimePeriod.PM
onConfirm(selectedPeriod, hourPickerState.selectedItem, minutePickerState.selectedItem)
},
text = stringResource(R.string.bottom_sheet_notification_time_change_confirm),
enabled = true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import com.sopt.clody.R
import com.sopt.clody.presentation.utils.extension.convertKSTtoUTZ
import com.sopt.clody.ui.theme.ClodyTheme

@Composable
Expand All @@ -22,6 +23,8 @@ fun NotificationTimeSelector(
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val timeUST = convertKSTtoUTZ(time)

Row(
modifier = modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
Expand All @@ -41,7 +44,7 @@ fun NotificationTimeSelector(
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = time,
text = stringResource(R.string.notification_setting_selected_time, timeUST.first.getLabel(), timeUST.second, timeUST.third),
style = ClodyTheme.typography.body3Medium,
color = ClodyTheme.colors.gray05,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ import com.sopt.clody.presentation.ui.setting.notificationsetting.component.Noti
import com.sopt.clody.presentation.ui.setting.notificationsetting.component.NotificationSwitch
import com.sopt.clody.presentation.ui.setting.notificationsetting.component.NotificationTimeSelector
import com.sopt.clody.presentation.utils.base.ClodyPreview
import com.sopt.clody.presentation.utils.extension.convertTo12HourFormat
import com.sopt.clody.ui.theme.ClodyTheme

@Composable
Expand Down Expand Up @@ -66,11 +65,13 @@ fun NotificationSettingRoute(
)

if (showNotificationTimePicker) {
ClodyPopupBottomSheet(onDismissRequest = { showNotificationTimePicker = false }) {
ClodyPopupBottomSheet(
onDismissRequest = { showNotificationTimePicker = false },
) {
NotificationSettingTimePicker(
onDismissRequest = { showNotificationTimePicker = false },
onConfirm = { newNotificationTime ->
notificationSettingViewModel.changeNotificationTime(context, newNotificationTime)
onConfirm = { timePeriod, hour, minute ->
notificationSettingViewModel.changeNotificationTime(context, timePeriod, hour, minute)
showNotificationTimePicker = false
},
)
Expand Down Expand Up @@ -153,7 +154,7 @@ fun NotificationSettingScreen(
)
Spacer(modifier = Modifier.height(24.dp))
NotificationTimeSelector(
time = notificationTime.convertTo12HourFormat(),
time = notificationTime,
onClick = onClickNotificationTime,
)
Spacer(modifier = Modifier.height(24.dp))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import com.sopt.clody.data.remote.dto.request.SendNotificationRequestDto
import com.sopt.clody.data.remote.util.NetworkUtil
import com.sopt.clody.domain.Notification
import com.sopt.clody.domain.repository.NotificationRepository
import com.sopt.clody.presentation.utils.extension.TimePeriod
import com.sopt.clody.presentation.utils.extension.convertUTZtoKST
import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_NETWORK_MESSAGE
import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_TEMPORARY_MESSAGE
import com.sopt.clody.presentation.utils.network.ErrorMessages.UNKNOWN_ERROR
Expand Down Expand Up @@ -140,7 +142,7 @@ class NotificationSettingViewModel @Inject constructor(
}
}

fun changeNotificationTime(context: Context, time: String) {
fun changeNotificationTime(context: Context, timePeriod: TimePeriod, hour: String, minute: String) {
_notificationTimeChangeState.value = NotificationTimeChangeState.Loading
viewModelScope.launch {
if (!networkUtil.isNetworkAvailable()) {
Expand All @@ -157,7 +159,7 @@ class NotificationSettingViewModel @Inject constructor(
isDiaryAlarm = _diaryAlarm.value,
isDraftAlarm = _draftAlarm.value,
isReplyAlarm = _replyAlarm.value,
time = time,
time = convertUTZtoKST(timePeriod, hour, minute),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify timezone conversion function robustness

The conversion from user's timezone to KST using convertUTZtoKST is correct for API communication. However, please verify that the conversion function handles edge cases properly:

  1. Timezone transitions (DST changes)
  2. Invalid input parameters
  3. Timezone offset calculations

🏁 Script executed:

#!/bin/bash
# Find and examine the convertUTZtoKST function implementation
ast-grep --pattern 'fun convertUTZtoKST($_, $_, $_) { $$$ }'

# Also check for any error handling in the conversion functions
fd . -name "TimeZoneExt.kt" | head -1 | xargs cat

Length of output: 343


🏁 Script executed:

#!/bin/bash
# 1. Locate the implementation of convertUTZtoKST
rg --context 5 'fun convertUTZtoKST' -n

# 2. Find and print the contents of TimeZoneExt.kt (first 200 lines) to inspect any helper functions or error handling
find . -type f -name 'TimeZoneExt.kt' -print -exec sed -n '1,200p' {} \;

Length of output: 3182


Fix convertUTZtoKST conversion logic and formatting

The current implementation mistakenly concatenates the hour string with 12 (e.g. "1" + 12 → "112") and uses %02s for formatting, which doesn’t zero-pad numbers. Update the function as follows:

• File: app/src/main/java/com/sopt/clody/presentation/utils/extension/TimeZoneExt.kt
• Method: convertUTZtoKST

 fun convertUTZtoKST(timePeriod: TimePeriod, hour: String, minute: String): String {
     val userZoneId = ZoneId.systemDefault()
     val kstZoneId = ZoneId.of("Asia/Seoul")

-    val hour24 = when (timePeriod) {
-        TimePeriod.AM -> if (hour == "12") 0 else hour.toInt()
-        TimePeriod.PM -> if (hour == "12") 12 else (hour + 12).toInt()
-    }
+    val hour24 = when (timePeriod) {
+        TimePeriod.AM -> if (hour == "12") 0 else hour.toInt()
+        TimePeriod.PM -> if (hour == "12") 12 else hour.toInt() + 12
+    }

     val userTime = LocalTime.of(hour24, minute.toInt())
     val userZoned = ZonedDateTime.of(LocalDate.now(userZoneId), userTime, userZoneId)
     val kstZoned = userZoned.withZoneSameInstant(kstZoneId)

     val kstHour = kstZoned.hour
     val kstMinute = kstZoned.minute

-    return String.format("%02s:%02s", kstHour, kstMinute)
+    return String.format("%02d:%02d", kstHour, kstMinute)
 }

Additional recommendations:

  • Change hour and minute parameters to Int to avoid unhandled NumberFormatException.
  • Optionally wrap parsing in a try/catch or validate inputs before calling.
🤖 Prompt for AI Agents
In
app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingViewModel.kt
at line 162, the call to convertUTZtoKST passes hour and minute as strings
causing incorrect concatenation and formatting issues. To fix this, update the
convertUTZtoKST function in
app/src/main/java/com/sopt/clody/presentation/utils/extension/TimeZoneExt.kt to
accept hour and minute as Int parameters, correctly perform arithmetic for time
conversion, and use proper zero-padding formatting (e.g., %02d). Also, add input
validation or try/catch around parsing to handle potential NumberFormatException
safely.

fcmToken = fcmToken,
)
notificationRepository.sendNotification(requestDto).fold(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.sopt.clody.presentation.utils.extension

import java.time.LocalDate
import java.time.LocalTime
import java.time.ZoneId
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter

/**
* @param time 서버로부터 수신받은 시간으로 "21:30" 와 같은 형태로 전달받는다.
* */
fun convertKSTtoUTZ(time: String): Triple<TimePeriod, Int, Int> {
val kstZoneId = ZoneId.of("Asia/Seoul")
val userZoneId = ZoneId.systemDefault()

val formatter = DateTimeFormatter.ofPattern("HH:mm")
val localTime = LocalTime.parse(time, formatter)

val kstDateTime = ZonedDateTime.of(LocalDate.now(kstZoneId), localTime, kstZoneId)
val userDateTime = kstDateTime.withZoneSameInstant(userZoneId)

val hour24 = userDateTime.hour
val minute = userDateTime.minute
val timePeriod = if (hour24 < 12) TimePeriod.AM else TimePeriod.PM
val hour12 = when {
hour24 == 0 -> 12
hour24 > 12 -> hour24 - 12
else -> hour24
}

return Triple(timePeriod, hour12, minute)
}

/**
* @param timePeriod 오전/오후
* @param hour 시간
* @param minute 분
* */
fun convertUTZtoKST(timePeriod: TimePeriod, hour: String, minute: String): String {
val userZoneId = ZoneId.systemDefault()
val kstZoneId = ZoneId.of("Asia/Seoul")

val hour24 = when (timePeriod) {
TimePeriod.AM -> if (hour == "12") 0 else hour.toInt()
TimePeriod.PM -> if (hour == "12") 12 else (hour + 12).toInt()
}

val userTime = LocalTime.of(hour24, minute.toInt())
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

Add input validation for minute parameter.

The minute parameter is converted to integer without validation, which could cause a NumberFormatException.

Apply this diff to add minute validation:

-    val userTime = LocalTime.of(hour24, minute.toInt())
+    val minuteInt = minute.toIntOrNull() ?: throw IllegalArgumentException("Invalid minute format: $minute")
+    require(minuteInt in 0..59) { "Invalid minute: $minute. Must be 0-59." }
+    val userTime = LocalTime.of(hour24, minuteInt)
📝 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
val userTime = LocalTime.of(hour24, minute.toInt())
val minuteInt = minute.toIntOrNull()
?: throw IllegalArgumentException("Invalid minute format: $minute")
require(minuteInt in 0..59) { "Invalid minute: $minute. Must be 0-59." }
val userTime = LocalTime.of(hour24, minuteInt)
🤖 Prompt for AI Agents
In app/src/main/java/com/sopt/clody/presentation/utils/extension/TimeZoneExt.kt
at line 52, the minute parameter is converted to an integer without validation,
risking a NumberFormatException. Add input validation to ensure the minute
string can be safely converted to an integer before calling LocalTime.of. If the
conversion fails or the value is out of the valid range (0-59), handle the error
appropriately, such as by returning a default value or throwing a controlled
exception.

val userZoned = ZonedDateTime.of(LocalDate.now(userZoneId), userTime, userZoneId)
val kstZoned = userZoned.withZoneSameInstant(kstZoneId)

val kstHour = kstZoned.hour
val kstMinute = kstZoned.minute

return String.format("%02s:%02s", kstHour, kstMinute)
}
2 changes: 1 addition & 1 deletion app/src/main/res/values-ko/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@
<string name="notification_setting_write_diary">일기 작성 알림 받기</string>
<string name="notification_setting_draft_diary">이어쓰기 알림 받기</string>
<string name="notification_setting_notification_time">알림 시간</string>
<string name="notification_setting_selected_time">%1$s %2$d시 %3$s분</string>
<string name="notification_setting_selected_time">%1$s %2$d시 %3$d분</string>
<string name="notification_setting_reply_diary">답장 도착 알림 받기</string>

<!-- 사용화면 : 오류 화면 -->
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@
<string name="notification_setting_write_diary">Journal Reminder</string>
<string name="notification_setting_draft_diary">Draft Reminder</string>
<string name="notification_setting_notification_time">Reminder Time</string>
<string name="notification_setting_selected_time">%2$d:%3$s %1$s</string>
<string name="notification_setting_selected_time">%2$d:%3$d %1$s</string>
<string name="notification_setting_reply_diary">Reply Notification</string>

<!-- Usage : ErrorScreen -->
Expand Down