Skip to content

Conversation

@SYAAINN
Copy link
Contributor

@SYAAINN SYAAINN commented Jun 2, 2025

📌 ISSUE

closed #266

📄 Work Description

1. 알림설정 화면에 이어쓰기 알림설정 추가한 UI 구현


2. 코드 리팩토링

  • 컴포넌트와 Screen에 흩어져있던 상태 및 동작들을 Route 단으로 호이스팅하여 책임 분리를 명확하게 했습니다.

✨ PR Point

아직 테스트를 하지는 못했으나, 기능적으로 이상은 .. 없지 않을까 합니다!
추후 MVI 마이그레이션 생각해서 최대한 Intent도 모으고 하는 식으로 리팩토링 진행하였습니다!

📸 ScreenShot/Video

스크린샷 2025-06-02 오후 11 55 02

Summary by CodeRabbit

  • New Features

    • Added support for draft diary notification alarms, allowing users to enable or disable alerts for draft entries in notification settings.
    • Introduced a new label for draft diary notifications in the notification settings screen.
  • Improvements

    • Notification settings screen now features a more streamlined and reusable interface for managing diary, draft, and reply alarms, as well as notification time.
    • Enhanced time selection with improved format conversion between 12-hour and 24-hour displays.
  • Refactor

    • Simplified and unified notification setting state management for a more consistent user experience.
    • Updated and generalized UI components for notification switches and time selectors.

SYAAINN added 2 commits June 2, 2025 14:33
- 컴포넌트 개선 : 각 알람 종류에 따라 별개였던 토글 스위치 컴포넌트 단일화, 알람 시간 선택 및 피커 부분 일부 개선
- 상태 호이스팅 : 뷰모델 관련한 부분 전부 Route로 끌어올려 Stateless 하게 호이스팅. 책임분리 및 가독성 향상
@SYAAINN SYAAINN requested a review from MoonsuKang June 2, 2025 14:59
@SYAAINN SYAAINN self-assigned this Jun 2, 2025
@SYAAINN SYAAINN added ✨ FEAT 기능 개발 🔥 민재 민재 labels Jun 2, 2025
@coderabbitai
Copy link

coderabbitai bot commented Jun 2, 2025

"""

Walkthrough

This update introduces a "draft alarm" (이어쓰기 알림) toggle to the notification settings UI and underlying logic. It adds new Boolean fields in relevant DTOs, updates the ViewModel and UI components to support the new alarm, and refactors the notification setting screen for improved modularity and clarity. Supporting string resources and time conversion utilities are also included.

Changes

File(s) / Group Change Summary
.../dto/request/SendNotificationRequestDto.kt Added isDraftAlarm Boolean property to request DTO.
.../dto/response/NotificationInfoResponseDto.kt,
.../dto/response/SendNotificationResponseDto.kt
Added isDraftAlarm Boolean property to response DTOs.
.../ui/auth/timereminder/TimeReminderViewModel.kt Set isDraftAlarm in notification request to match isPermissionGranted.
.../ui/setting/notificationsetting/component/NotificationSettingTimePicker.kt Refactored time picker: removed ViewModel dependency, renamed callback, used extension for time format.
.../ui/setting/notificationsetting/component/NotificationSwitch.kt Renamed and generalized switch component; removed ViewModel/context dependencies.
.../ui/setting/notificationsetting/component/NotificationTimeSelector.kt Renamed and generalized time selector component; updated parameters and layout.
.../ui/setting/notificationsetting/component/ReplyAlarmSwitch.kt Deleted specialized reply alarm switch component.
.../ui/setting/notificationsetting/screen/DiaryAlarmChangeState.kt,
.../ui/setting/notificationsetting/screen/ReplyAlarmChangeState.kt
Deleted old alarm change state sealed classes.
.../ui/setting/notificationsetting/screen/NotificationChangeState.kt Added new unified sealed class for notification change state.
.../ui/setting/notificationsetting/screen/NotificationSettingScreen.kt Refactored screen: lifted state, replaced specialized switches with generic ones, updated UI logic.
.../ui/setting/notificationsetting/screen/NotificationSettingViewModel.kt Refactored ViewModel: unified state, added draft alarm logic, simplified state flows and change methods.
.../utils/extension/StringExt.kt Added string extension functions for 12/24-hour time conversion.
.../res/values/strings.xml Added string resource for "draft diary notification" label.
.../domain/Notification.kt Added enum class Notification with DIARY, DRAFT, REPLY constants.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant NotificationSettingScreen
    participant NotificationSettingViewModel
    participant Server

    User->>NotificationSettingScreen: Toggle "Draft Alarm" switch
    NotificationSettingScreen->>NotificationSettingViewModel: onClickDraftSwitch()
    NotificationSettingViewModel->>NotificationSettingViewModel: Toggle draftAlarm state
    NotificationSettingViewModel->>Server: Send updated notification settings (incl. isDraftAlarm)
    Server-->>NotificationSettingViewModel: Response (success/failure)
    NotificationSettingViewModel-->>NotificationSettingScreen: Update UI state
Loading

Assessment against linked issues

Objective (issue #) Addressed Explanation
Add a toggle for "draft alarm" (이어쓰기 알림) in the notification settings UI (#266)

Assessment against linked issues: Out-of-scope changes

No out-of-scope changes found.

Poem

🐇✨
A toggle for drafts, now shining so bright,
In settings it sits, a new bunny delight.
With switches and flows, the code hops anew,
Time formats convert, alarms set for you.
"이어쓰기 알림" joins the notification crew!

— A celebratory rabbit
"""

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🔭 Outside diff range comments (1)
app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingViewModel.kt (1)

92-128: 🛠️ Refactor suggestion

Significant code duplication across alarm change methods.

The three alarm change methods (changeDiaryAlarm, changeDraftAlarm, changeReplyAlarm) have nearly identical implementations with only the toggled alarm state being different. This violates the DRY principle.

Consider refactoring to a single method:

+    enum class AlarmType {
+        DIARY, DRAFT, REPLY
+    }
+
+    fun changeAlarmState(context: Context, alarmType: AlarmType) {
+        _notificationChangeState.value = NotificationChangeState.Loading
+        viewModelScope.launch {
+            if (!networkUtil.isNetworkAvailable()) {
+                _failureDialogMessage.value = FAILURE_NETWORK_MESSAGE
+                _showFailureDialog.value = true
+                return@launch
+            }
+            val fcmToken = getTokenFromPreferences(context)
+            if (fcmToken.isNullOrBlank()) {
+                _notificationChangeState.value = NotificationChangeState.Failure("FCM Token을 가져오는데 실패했습니다.")
+                return@launch
+            }
+            
+            val (isDiaryAlarm, isDraftAlarm, isReplyAlarm) = when (alarmType) {
+                AlarmType.DIARY -> Triple(!_diaryAlarm.value, _draftAlarm.value, _replyAlarm.value)
+                AlarmType.DRAFT -> Triple(_diaryAlarm.value, !_draftAlarm.value, _replyAlarm.value)
+                AlarmType.REPLY -> Triple(_diaryAlarm.value, _draftAlarm.value, !_replyAlarm.value)
+            }
+            
+            val requestDto = SendNotificationRequestDto(
+                isDiaryAlarm = isDiaryAlarm,
+                isDraftAlarm = isDraftAlarm,
+                isReplyAlarm = isReplyAlarm,
+                time = _notificationTime.value,
+                fcmToken = fcmToken,
+            )
+            notificationRepository.sendNotification(requestDto).fold(
+                onSuccess = {
+                    _notificationChangeState.value = NotificationChangeState.Success(it)
+                    getNotificationInfo()
+                },
+                onFailure = {
+                    _failureDialogMessage.value = if (it.message?.contains("200") == false) {
+                        FAILURE_TEMPORARY_MESSAGE
+                    } else {
+                        UNKNOWN_ERROR
+                    }
+                    _showFailureDialog.value = true
+                    _notificationChangeState.value = NotificationChangeState.Failure(_failureDialogMessage.value)
+                },
+            )
+        }
+    }

Then replace the individual methods with delegating calls or remove them entirely and update the UI to use the new method.

Also applies to: 130-166, 206-242

🧹 Nitpick comments (3)
app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingViewModel.kt (3)

54-57: Consider moving the initial data fetch out of the init block.

Triggering network calls in the init block can lead to unnecessary resource usage. Consider calling getNotificationInfo() from the UI layer when the screen is ready to display data.

Remove the init block and let the UI trigger the initial data fetch:

-    init {
-        getNotificationInfo()
-    }

Then call getNotificationInfo() from the NotificationSettingRoute when the composable enters the composition.


253-256: Consider caching the FCM token.

The FCM token is retrieved from SharedPreferences in multiple methods. Since FCM tokens don't change frequently during a session, consider caching it to avoid repeated SharedPreferences access.

+    private var cachedFcmToken: String? = null
+    
     private fun getTokenFromPreferences(context: Context): String? {
+        cachedFcmToken?.let { return it }
         val sharedPreferences = context.getSharedPreferences("fcm_prefs", Context.MODE_PRIVATE)
-        return sharedPreferences.getString("fcm_token", null)
+        return sharedPreferences.getString("fcm_token", null).also {
+            cachedFcmToken = it
+        }
     }

244-246: Add reset method for notification change state.

For consistency with resetNotificationTimeChangeState(), consider adding a reset method for notificationChangeState.

     fun resetNotificationTimeChangeState() {
         _notificationTimeChangeState.value = NotificationTimeChangeState.Idle
     }
+    
+    fun resetNotificationChangeState() {
+        _notificationChangeState.value = NotificationChangeState.Idle
+    }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 90f1c25 and 3ebcec5.

📒 Files selected for processing (15)
  • app/src/main/java/com/sopt/clody/data/remote/dto/request/SendNotificationRequestDto.kt (1 hunks)
  • app/src/main/java/com/sopt/clody/data/remote/dto/response/NotificationInfoResponseDto.kt (1 hunks)
  • app/src/main/java/com/sopt/clody/data/remote/dto/response/SendNotificationResponseDto.kt (1 hunks)
  • app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderViewModel.kt (1 hunks)
  • app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationSettingTimePicker.kt (2 hunks)
  • app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationSwitch.kt (1 hunks)
  • app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationTimeSelector.kt (2 hunks)
  • app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/ReplyAlarmSwitch.kt (0 hunks)
  • app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/DiaryAlarmChangeState.kt (0 hunks)
  • app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationChangeState.kt (1 hunks)
  • app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingScreen.kt (4 hunks)
  • app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingViewModel.kt (7 hunks)
  • app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/ReplyAlarmChangeState.kt (0 hunks)
  • app/src/main/java/com/sopt/clody/presentation/utils/extension/StringExt.kt (1 hunks)
  • app/src/main/res/values/strings.xml (1 hunks)
💤 Files with no reviewable changes (3)
  • app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/DiaryAlarmChangeState.kt
  • app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/ReplyAlarmChangeState.kt
  • app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/ReplyAlarmSwitch.kt
🧰 Additional context used
🧬 Code Graph Analysis (1)
app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingScreen.kt (7)
app/src/main/java/com/sopt/clody/presentation/ui/component/popup/ClodyPopupBottomSheet.kt (1)
  • ClodyPopupBottomSheet (28-77)
app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationSettingTimePicker.kt (1)
  • NotificationSettingTimePicker (34-155)
app/src/main/java/com/sopt/clody/presentation/ui/setting/component/SettingTopAppBar.kt (1)
  • SettingTopAppBar (15-38)
app/src/main/java/com/sopt/clody/presentation/ui/component/LoadingScreen.kt (1)
  • LoadingScreen (15-30)
app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationSwitch.kt (1)
  • NotificationSwitch (20-56)
app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationTimeSelector.kt (1)
  • NotificationTimeSelector (19-54)
app/src/main/java/com/sopt/clody/presentation/ui/component/FailureScreen.kt (1)
  • FailureScreen (27-69)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: build
🔇 Additional comments (17)
app/src/main/res/values/strings.xml (1)

130-130: LGTM! Well-integrated string resource addition.

The new string resource follows the existing naming convention and properly supports the draft diary notification feature. The Korean text is clear and consistent with the app's terminology.

app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderViewModel.kt (1)

48-48: LGTM! Consistent initialization of draft alarm permission.

The addition properly initializes the draft alarm flag with the same permission state as other notification types, maintaining logical consistency in the initial setup flow.

app/src/main/java/com/sopt/clody/data/remote/dto/response/NotificationInfoResponseDto.kt (1)

9-9: LGTM! Proper DTO extension for draft alarm support.

The new property follows the established pattern with correct serialization annotation and appropriate Boolean type. This properly extends the notification info response to support the draft alarm feature.

app/src/main/java/com/sopt/clody/data/remote/dto/response/SendNotificationResponseDto.kt (1)

9-9: LGTM! Consistent DTO extension.

The new isDraftAlarm property follows the established pattern with proper serialization annotation and logical positioning between diary and reply alarm flags.

app/src/main/java/com/sopt/clody/data/remote/dto/request/SendNotificationRequestDto.kt (1)

9-9:

Details

✅ Verification successful

LGTM! Consistent with response DTO.

The new isDraftAlarm property maintains consistency with the response DTO structure and follows established patterns.

Verify that all code that constructs SendNotificationRequestDto has been updated to include the new isDraftAlarm parameter:


🏁 Script executed:

#!/bin/bash
# Description: Find all usages of SendNotificationRequestDto to ensure they include the new isDraftAlarm parameter
# Expected: All constructor calls should have 5 parameters now

# Search for SendNotificationRequestDto constructor calls
ast-grep --pattern 'SendNotificationRequestDto($$$)'

# Also search for any instantiation patterns
rg -A 3 "SendNotificationRequestDto"

Length of output: 15748


Approval: All SendNotificationRequestDto instantiations include isDraftAlarm

I’ve verified that every constructor call of SendNotificationRequestDto now supplies the new isDraftAlarm parameter in:

  • TimeReminderViewModel.kt
  • NotificationSettingViewModel.kt (all occurrences)

No further changes are required.

app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationChangeState.kt (1)

5-10:

Details

✅ Verification successful

Excellent state consolidation refactoring!

The unified NotificationChangeState sealed class properly consolidates notification change operations into a single, well-structured state representation. This follows standard async operation patterns and should simplify state management.

Verify that the old separate state classes are no longer referenced in the codebase:


🏁 Script executed:

#!/bin/bash
# Description: Ensure old DiaryAlarmChangeState and ReplyAlarmChangeState are no longer used
# Expected: No references to the old state classes should be found

# Search for old state class references
rg "DiaryAlarmChangeState|ReplyAlarmChangeState"

# Also check for any remaining imports
rg -A 2 -B 2 "import.*DiaryAlarmChangeState|import.*ReplyAlarmChangeState"

Length of output: 127


Great refactoring—old state classes successfully removed

No references to DiaryAlarmChangeState or ReplyAlarmChangeState were found in the codebase after running a global search. The new NotificationChangeState sealed class cleanly consolidates all notification state management. Approving these changes.

app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationTimeSelector.kt (3)

20-24: Excellent refactoring to stateless component!

The parameter changes make this component more reusable and follow Compose best practices by removing internal state management. The addition of the modifier parameter with a default value improves flexibility.


26-26: Good modifier application pattern.

Applying the passed modifier to the root composable allows for better composition and styling flexibility from parent components.


37-37: Improved callback design.

Using the passed onClick lambda directly instead of a hardcoded function call makes the component more flexible and reusable.

app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationSettingTimePicker.kt (1)

31-31: LGTM! Good refactoring to improve component reusability.

The changes successfully decouple the time picker from the ViewModel by:

  • Using the to24HourFormat extension function for time conversion
  • Simplifying the callback interface to accept a formatted time string
  • Keeping the conversion logic local to the component

This aligns well with the broader refactoring effort to lift state management to the Route layer.

Also applies to: 37-37, 140-146

app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationSwitch.kt (1)

21-26: Excellent refactoring to a generic, reusable component!

The transformation from DiaryAlarmSwitch to NotificationSwitch successfully:

  • Removes tight coupling to ViewModel and Context
  • Uses @StringRes for type-safe string resources
  • Simplifies the parameter interface

Note: The switch scale was reduced from 0.8f to 0.7f. Please verify this change maintains visual consistency across the app.

Also applies to: 40-40

app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingScreen.kt (3)

44-47: Great separation of concerns in the Route composable!

The refactoring successfully:

  • Collects individual state flows for better granularity
  • Simplifies callbacks by removing unnecessary parameters
  • Properly delegates UI state to the stateless Screen composable

This makes the state management more explicit and easier to test.

Also applies to: 61-65


108-121: Excellent implementation of a stateless, testable Screen composable!

The refactoring demonstrates best practices:

  • Pure composable function with no side effects
  • Clear parameter naming and organization
  • Proper integration of the new draft alarm feature
  • Consistent UI spacing and structure

The use of convertTo12HourFormat() extension maintains consistency with the time picker's 24-hour format output.

Also applies to: 145-166


181-206: Well-structured preview for UI development!

The preview composable properly demonstrates the notification settings UI with realistic test data, making it easy to verify the layout and interactions during development.

app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingViewModel.kt (3)

24-35: State flow declarations look good!

The new state flows for individual alarm types and notification time follow the proper pattern and align well with the PR objective of adding draft alarm functionality.


70-74: Proper state updates from notification info.

The method correctly updates all individual alarm states and notification time from the response.


182-191: Good adaptation to use current alarm states.

The method correctly uses the current alarm states from the state flows when sending the notification update request.

Comment on lines +3 to +17
fun String.convertTo12HourFormat(): String {
val (hourBefore, minuteBefore) = this.split(":").map { it.toInt() }

val amPm = if (hourBefore < 12) "오전" else "오후"

val hourAfter = when {
hourBefore == 0 -> 12
hourBefore > 12 -> hourBefore - 12
else -> hourBefore
}

val minuteAfter = if (minuteBefore == 0) "00" else minuteBefore

return String.format("$amPm ${hourAfter}${minuteAfter}")
}
Copy link

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 and error handling for robust time parsing.

The function lacks input validation and could throw NumberFormatException if the input string doesn't match the expected "HH:mm" format. Additionally, there's a minor formatting inconsistency in the minute handling.

Apply this diff to improve error handling and consistency:

 fun String.convertTo12HourFormat(): String {
+    if (!this.matches(Regex("\\d{1,2}:\\d{1,2}"))) {
+        throw IllegalArgumentException("Invalid time format. Expected HH:mm format.")
+    }
+    
     val (hourBefore, minuteBefore) = this.split(":").map { it.toInt() }
+    
+    if (hourBefore !in 0..23 || minuteBefore !in 0..59) {
+        throw IllegalArgumentException("Invalid time values. Hour must be 0-23, minute must be 0-59.")
+    }

     val amPm = if (hourBefore < 12) "오전" else "오후"

     val hourAfter = when {
         hourBefore == 0 -> 12
         hourBefore > 12 -> hourBefore - 12
         else -> hourBefore
     }

-    val minuteAfter = if (minuteBefore == 0) "00" else minuteBefore
+    val minuteAfter = String.format("%02d", minuteBefore)

     return String.format("$amPm ${hourAfter}시 ${minuteAfter}분")
 }
📝 Committable suggestion

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

Suggested change
fun String.convertTo12HourFormat(): String {
val (hourBefore, minuteBefore) = this.split(":").map { it.toInt() }
val amPm = if (hourBefore < 12) "오전" else "오후"
val hourAfter = when {
hourBefore == 0 -> 12
hourBefore > 12 -> hourBefore - 12
else -> hourBefore
}
val minuteAfter = if (minuteBefore == 0) "00" else minuteBefore
return String.format("$amPm ${hourAfter}${minuteAfter}")
}
fun String.convertTo12HourFormat(): String {
if (!this.matches(Regex("\\d{1,2}:\\d{1,2}"))) {
throw IllegalArgumentException("Invalid time format. Expected HH:mm format.")
}
val (hourBefore, minuteBefore) = this.split(":").map { it.toInt() }
if (hourBefore !in 0..23 || minuteBefore !in 0..59) {
throw IllegalArgumentException("Invalid time values. Hour must be 0-23, minute must be 0-59.")
}
val amPm = if (hourBefore < 12) "오전" else "오후"
val hourAfter = when {
hourBefore == 0 -> 12
hourBefore > 12 -> hourBefore - 12
else -> hourBefore
}
val minuteAfter = String.format("%02d", minuteBefore)
return String.format("$amPm ${hourAfter}${minuteAfter}")
}
🤖 Prompt for AI Agents
In app/src/main/java/com/sopt/clody/presentation/utils/extension/StringExt.kt
around lines 3 to 17, the convertTo12HourFormat function lacks input validation
and error handling, which can cause NumberFormatException if the input is not in
"HH:mm" format. Add validation to check if the string splits into exactly two
parts and both parts are valid integers within appropriate ranges (0-23 for
hours, 0-59 for minutes). Also, fix the minute formatting to always display two
digits by formatting minuteAfter as a two-digit string instead of conditionally
replacing 0 with "00". Wrap parsing logic in a try-catch block to handle
exceptions gracefully and return a default or error string if parsing fails.

Comment on lines +19 to +27
fun Triple<String, String, String>.to24HourFormat(): String {
val (amPm, hour, minute) = this
val hourInt = when {
amPm == "오후" && hour.toInt() != 12 -> hour.toInt() + 12
amPm == "오전" && hour.toInt() == 12 -> 0
else -> hour.toInt()
}
return String.format("%02d:%02d", hourInt, minute.toInt())
}
Copy link

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 the Triple conversion function.

The function should validate the input Triple components to prevent runtime errors and ensure data integrity.

Apply this diff to add proper validation:

 fun Triple<String, String, String>.to24HourFormat(): String {
     val (amPm, hour, minute) = this
+    
+    if (amPm !in listOf("오전", "오후")) {
+        throw IllegalArgumentException("Invalid AM/PM value. Expected '오전' or '오후'.")
+    }
+    
+    val hourInt = hour.toIntOrNull() ?: throw IllegalArgumentException("Invalid hour format.")
+    val minuteInt = minute.toIntOrNull() ?: throw IllegalArgumentException("Invalid minute format.")
+    
+    if (hourInt !in 1..12 || minuteInt !in 0..59) {
+        throw IllegalArgumentException("Invalid time values. Hour must be 1-12, minute must be 0-59.")
+    }
+    
-    val hourInt = when {
-        amPm == "오후" && hour.toInt() != 12 -> hour.toInt() + 12
-        amPm == "오전" && hour.toInt() == 12 -> 0
-        else -> hour.toInt()
+    val hour24 = when {
+        amPm == "오후" && hourInt != 12 -> hourInt + 12
+        amPm == "오전" && hourInt == 12 -> 0
+        else -> hourInt
     }
-    return String.format("%02d:%02d", hourInt, minute.toInt())
+    return String.format("%02d:%02d", 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
fun Triple<String, String, String>.to24HourFormat(): String {
val (amPm, hour, minute) = this
val hourInt = when {
amPm == "오후" && hour.toInt() != 12 -> hour.toInt() + 12
amPm == "오전" && hour.toInt() == 12 -> 0
else -> hour.toInt()
}
return String.format("%02d:%02d", hourInt, minute.toInt())
}
fun Triple<String, String, String>.to24HourFormat(): String {
val (amPm, hour, minute) = this
if (amPm !in listOf("오전", "오후")) {
throw IllegalArgumentException("Invalid AM/PM value. Expected '오전' or '오후'.")
}
val hourInt = hour.toIntOrNull() ?: throw IllegalArgumentException("Invalid hour format.")
val minuteInt = minute.toIntOrNull() ?: throw IllegalArgumentException("Invalid minute format.")
if (hourInt !in 1..12 || minuteInt !in 0..59) {
throw IllegalArgumentException("Invalid time values. Hour must be 1-12, minute must be 0-59.")
}
val hour24 = when {
amPm == "오후" && hourInt != 12 -> hourInt + 12
amPm == "오전" && hourInt == 12 -> 0
else -> hourInt
}
return String.format("%02d:%02d", hour24, minuteInt)
}
🤖 Prompt for AI Agents
In app/src/main/java/com/sopt/clody/presentation/utils/extension/StringExt.kt
around lines 19 to 27, the to24HourFormat function lacks input validation for
the Triple components, which can cause runtime errors. Add validation to check
that amPm is either "오전" or "오후", and that hour and minute strings can be safely
converted to integers within valid ranges (hour 1-12, minute 0-59). If
validation fails, handle it gracefully by throwing an appropriate exception or
returning a default value.

Copy link
Contributor

@MoonsuKang MoonsuKang left a comment

Choose a reason for hiding this comment

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

수고하셨습니다~!! 궁금한 점 몇가지 리뷰 남겼습니다~

},
modifier = Modifier.scale(0.8f),
checked = checkedState,
onCheckedChange = { onClick() },
Copy link
Contributor

Choose a reason for hiding this comment

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

P3
Switch Component의 Semantic한 핸들러는 onCheckedChange: ((Boolean) -> Unit)?, 인데 onClick? 이 조금 의미가 다를 수 있다고 생각합니데이.
parameter를 onclick -> onCheckedChange: (Boolean) -> Unit,
으로 하는게 어떨까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

좋다고 생각해서 변경하기는 했는데 한 가지 의문점이 생겼네요.
MVI 구조를 차용하게 되면 토글 스위치의 true, false 상태나 변경해야할 때 모두 화면의 State를 기반으로 할 것 같은데요,
그렇다보니 (Boolean) -> Unit 으로 선언할 수는 있지만 사실상 { newState -> ~ } 이런 형식으로 사용할 필요가 없다고 생각이 됩니다 .. 타입만 고치고 실제 구현상에서는 사용하지 않는 것에 대해 어떻게 생각하십니까 ..!

Copy link
Contributor

Choose a reason for hiding this comment

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

그쵸, 나중에 아마도 viewModel.onEvent(ToggleDiaryAlarm) 같은 형태로 Contract 기반의 이벤트를 전달하게 될 것 같네용.
Boolean 인자를 직접 쓰진 않더라도, Switch 컴포넌트의 시멘틱에 맞춰 onCheckedChange를 사용하는 게 구조적으로 더 자연스러워 보입니다.
그래서 의미상 onClick보다는 onCheckedChange로 표현해두는 게 좋겠다고 생각했습니다 ~

Comment on lines 72 to 76
onConfirm = { newNotificationTime ->
notificationInfo?.let {
notificationSettingViewModel.changeNotificationTime(context, it, newNotificationTime)
notificationSettingViewModel.changeNotificationTime(context, newNotificationTime)
}
updateNotificationTimePicker(false)
showNotificationTimePicker = false
Copy link
Contributor

Choose a reason for hiding this comment

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

p3

                onConfirm = { newNotificationTime ->
                    notificationSettingViewModel.changeNotificationTime(context, newNotificationTime)
                    showNotificationTimePicker = false
                },

이렇게 해도 되지 않을까요? notificationInfo 가 dead code 인거 같은데 혹시 아닌가요??

Copy link
Contributor Author

Choose a reason for hiding this comment

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

맞습니다 ㅎ Screen 단에서 사용하던거 호이스팅하면서 제거해도 됐는데 깜빡했네요!

Comment on lines 181 to 184
@Preview(showBackground = true)
@Composable
private fun PreviewNotificationSettingScreen() {
ClodyTheme {
Copy link
Contributor

Choose a reason for hiding this comment

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

P5
@ClodyPreview도 한번 사용해보면 좋습니다 ㅋㅎㅋㅎ

Copy link
Contributor Author

Choose a reason for hiding this comment

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

앗 ㅋ 앞으로 애용하겠습니다. 지금 써보니까 기가 막히네요

}

fun changeNotificationTime(context: Context, notificationInfo: NotificationInfoResponseDto, time: String) {
fun changeDraftAlarm(context: Context) {
Copy link
Contributor

Choose a reason for hiding this comment

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

P3

changeDraftAlarm, changeNotificationTime, changeReplyAlarm 이거 세개 코드가 80~90%는 동일한 거 같은데

뭔가 changeAlarm이라는 function 하나 만들어서

fun change???Alarm(context: Context) {
    changeAlarm(context) { copy(is???Alarm = !is???Alarm) }
}

이런식으로 하는거 어떻게 생각하시나요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

안그래도 그것때문에 불편했는데 추후 MVI 마이그레이션 때 제대로 바꾸려고 놔뒀습니다 .. ㅎ
지금은 중복 코드 제거할 수 있게 하나의 함수로 묶어보긴 했습니다!

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingScreen.kt (1)

106-119: Consider parameter object pattern for better maintainability.

While the stateless composable design is excellent, the large number of parameters (11 total) could become unwieldy as the feature grows. Consider using a data class to group related parameters.

+data class NotificationSettingScreenState(
+    val notificationInfoState: NotificationInfoState,
+    val diaryAlarm: Boolean,
+    val draftAlarm: Boolean,
+    val replyAlarm: Boolean,
+    val notificationTime: String,
+)
+
+data class NotificationSettingScreenCallbacks(
+    val onClickBack: () -> Unit,
+    val onClickDiarySwitch: (Boolean) -> Unit,
+    val onClickDraftSwitch: (Boolean) -> Unit,
+    val onClickNotificationTime: () -> Unit,
+    val onClickReplySwitch: (Boolean) -> Unit,
+    val onClickRetry: () -> Unit,
+)
+
 @Composable
 fun NotificationSettingScreen(
-    notificationInfoState: NotificationInfoState,
-    diaryAlarm: Boolean,
-    draftAlarm: Boolean,
-    replyAlarm: Boolean,
-    notificationTime: String,
-    onClickBack: () -> Unit,
-    onClickDiarySwitch: (Boolean) -> Unit,
-    onClickDraftSwitch: (Boolean) -> Unit,
-    onClickNotificationTime: () -> Unit,
-    onClickReplySwitch: (Boolean) -> Unit,
-    onClickRetry: () -> Unit,
+    state: NotificationSettingScreenState,
+    callbacks: NotificationSettingScreenCallbacks,
 ) {
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3ebcec5 and 6001a36.

📒 Files selected for processing (4)
  • app/src/main/java/com/sopt/clody/domain/Notification.kt (1 hunks)
  • app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationSwitch.kt (1 hunks)
  • app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingScreen.kt (3 hunks)
  • app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingViewModel.kt (6 hunks)
✅ Files skipped from review due to trivial changes (1)
  • app/src/main/java/com/sopt/clody/domain/Notification.kt
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationSwitch.kt
  • app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingViewModel.kt
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: build
🔇 Additional comments (7)
app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingScreen.kt (7)

24-24: LGTM: Clean import additions for new functionality.

The new imports are well-organized and necessary for the added draft alarm functionality and refactored components.

Also applies to: 32-36


45-48: Excellent state management separation.

The refactoring to collect individual alarm states directly from ViewModel StateFlows improves the architecture by making state dependencies explicit and the UI more reactive.


61-64: Clean callback implementation with proper domain abstraction.

The use of the Notification enum for the changeAlarm method calls provides good type safety and domain abstraction.


179-204: Great addition of preview functionality!

The preview composable follows best practices by using realistic data and the @ClodyPreview annotation as suggested in the past review comments.


93-93: Improved method naming for better clarity.

The change from a generic reset to resetNotificationTimeChangeState() provides better specificity about what state is being reset.


149-153:

Details

❓ Verification inconclusive

Verify string resource exists for draft alarm.

Ensure that the string resource R.string.notification_setting_draft_diary has been added to all localization files.


🏁 Script executed:

#!/bin/bash
# Description: Verify that the draft diary string resource exists in all string resource files
# Expected: The string resource should be present in all locale directories

echo "Checking for notification_setting_draft_diary string resource:"
fd -e xml -x grep -l "notification_setting_draft_diary" {} \;

echo -e "\nContent of the string resources:"
fd -e xml -x grep "notification_setting_draft_diary" {} \;

Length of output: 452


🏁 Script executed:

#!/bin/bash
# Verify that notification_setting_draft_diary exists in every locale-specific strings.xml
echo "Checking notification_setting_draft_diary in all values* directories:"
for xml in app/src/main/res/values*/strings*.xml; do
  echo "$xml"
  if grep -q "notification_setting_draft_diary" "$xml"; then
    grep -H "notification_setting_draft_diary" "$xml"
  else
    echo "  ✗ Missing in $xml"
  fi
done

Length of output: 550


Ensure string resource is localized across all supported locales

The resource notification_setting_draft_diary is present in your default strings file:

  • app/src/main/res/values/strings.xml
    <string name="notification_setting_draft_diary">이어쓰기 알림 받기</string>

If your app supports additional languages, please confirm that this entry has been added to each locale’s values-<locale>/strings.xml.


156-156:

Details

✅ Verification successful

Verify time format conversion works correctly.

Ensure the convertTo12HourFormat() extension function properly handles edge cases like midnight (00:00) and noon (12:00).


🏁 Script executed:

#!/bin/bash
# Description: Find and examine the convertTo12HourFormat extension function
# Expected: The function should handle 24-hour to 12-hour conversion correctly

echo "Finding convertTo12HourFormat extension function:"
ast-grep --pattern $'fun String.convertTo12HourFormat(): String {
  $$$
}'

echo -e "\nAlternatively searching with ripgrep:"
rg -A 10 "fun.*convertTo12HourFormat"

Length of output: 3046


Time conversion edge cases verified and correct

The convertTo12HourFormat() function maps:

  • “00:00” → “오전 12시 00분”
  • “12:00” → “오후 12시 00분”

No changes required.

Copy link
Contributor

@MoonsuKang MoonsuKang left a comment

Choose a reason for hiding this comment

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

수고하셨습니다이~

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ FEAT 기능 개발 🔥 민재 민재

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] 이어쓰기 알림설정을 추가한 UI를 구현합니다.

3 participants