Skip to content

Conversation

@uson1004
Copy link
Member

@uson1004 uson1004 commented Jan 5, 2026

๊ฐœ์š”

๋งˆ์ดํŽ˜์ด์ง€ ๊ตฌํ˜„

์ž‘์—…์‚ฌํ•ญ

  • ๋งˆ์ดํŽ˜์ด์ง€๋ฅผ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.
  • ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ •
  • ํ”„๋กœํ•„ ์‚ฌ์ง„ ๋ณ€๊ฒฝ
  • ํ‘ธ์‹œ ์•Œ๋ฆผ ์ผœ๊ธฐ
  • ๋กœ๊ทธ์•„์›ƒ

์ถ”๊ฐ€ ๋กœ ํ•  ๋ง

Summary by CodeRabbit

  • New Features

    • Profile image selection & adjustment (zoom, upload)
    • Point history screen with tabs (All/Bonus/Minus)
    • Password check & reset flows
    • Settings screen with notification toggle and logout
    • File upload support
  • Permissions

    • Added media and legacy storage read permissions for image access
  • Design & UI Updates

    • New settings icon and enriched symbol content
    • Button/item component visuals and interaction tweaks

โœ๏ธ Tip: You can customize this high-level summary in your review settings.

@uson1004 uson1004 requested a review from a team January 5, 2026 13:46
@uson1004 uson1004 self-assigned this Jan 5, 2026
@uson1004 uson1004 added the feat ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ ํ•  ๊ฒฝ์šฐ label Jan 5, 2026
@uson1004 uson1004 linked an issue Jan 5, 2026 that may be closed by this pull request
@coderabbitai
Copy link

coderabbitai bot commented Jan 5, 2026

๐Ÿ“ Walkthrough

Walkthrough

Adds profile image upload/adjust flows, point-history and password-reset screens with viewmodels, file/user repositories and DI bindings, design-system updates, datetime migration to kotlinx.datetime, Gradle dependencies, and two dev manifest permissions.

Changes

Cohort / File(s) Summary
Manifest & Versions
app/src/dev/AndroidManifest.xml, gradle/libs.versions.toml
Added android.permission.READ_MEDIA_IMAGES and android.permission.READ_EXTERNAL_STORAGE (maxSdkVersion=32); added accompanist and telephoto library/version entries.
App entry & Navigation
app/src/dev/kotlin/.../DmsApp.kt, app/src/dev/kotlin/.../MainActivity.kt
Added NavKeys (Setting, PointHistory, CheckPassword, ResetPassword, SelectProfile, AdjustProfile); changed DmsApp(isJwtAvailable) param from StateFlow<Boolean> โ†’ Boolean; wired new routes and updated MainActivity call.
Design system
core/design-system/.../button/Button.kt, .../DmsItemButton.kt, .../dialog/AlertDialog.kt, .../foundation/DmsIcon.kt, .../foundation/DmsSymbol.kt, core/design-system/.../res/drawable/ic_setting.xml
DmsLayeredButton shape defaulted; removed navigation inset padding; DmsItemButton icon API changed from Painter โ†’ @DrawableRes Int; added AlertDialog wrapper; added DmsIcon.Setting, replaced DmsSymbol with DmsSymbolContent; new ic_setting drawable.
File upload โ€” network & data
network/.../file/apiservice/FileApiService.kt, network/.../file/model/FetchFileUrlResponse.kt, network/.../file/datasource/*, data/.../file/*, data/build.gradle.kts
New FetchFileUrlResponse; uploadFile now returns FetchFileUrlResponse through datasource/service; added FileRepository, FileRepositoryImpl, PresignedFileUrl/FileUrl models, mappers, and DI binding; added kotlinx.datetime dep to data module.
User repository & DI
data/.../user/repository/*, data/.../user/di/RepositoryModule.kt
Added UserRepository abstraction, UserRepositoryImpl (edit/compare password), and Hilt binding module.
Date migration
data/.../point/model/Point.kt, data/.../point/mapper/PointMapper.kt, feature/build.gradle.kts
Switched Point.date to kotlinx.datetime.LocalDate; updated mapper parsing; added kotlinx.datetime to feature deps.
Profile flows & viewmodels
feature/.../profile/ui/SelectProfileScreen.kt, .../profile/ui/AdjustProfileScreen.kt, .../profile/viewmodel/SelectProfileViewModel.kt, .../profile/viewmodel/AdjustProfileViewModel.kt, data/.../file/*
New SelectProfile and AdjustProfile screens and viewmodels: media permission handling, gallery loading, image selection, presigned upload via FileRepository, zoom/adjust screen (telephoto).
Point history feature
feature/.../point/navigation/PointHistoryRoute.kt, .../point/ui/PointHistoryScreen.kt, .../point/viewmodel/PointHistoryViewModel.kt, .../point/ui/component/PointItem.kt
Added PointHistory route/screen/viewmodel with tabbed pager and per-tab lists.
MyPage & UI components
feature/.../mypage/*, feature/.../mypage/viewmodel/MyPageViewModel.kt
MyPage reworked to accept navigation callbacks; added ProfileContent, PhraseCard/Content, PointCards; new MyPageViewModel fetching profile and emitting side effects.
Settings & notification toggle
feature/.../setting/navigation/SettingRoute.kt, .../setting/ui/SettingScreen.kt, .../setting/ui/component/SettingRotateContent.kt, .../setting/viewmodel/SettingViewModel.kt
New Setting screen/route, SettingRotateContent component, viewmodel handling device token, notification status, sign-out, and related side effects.
Password reset flows
feature/.../resetpassword/navigation/*, .../resetpassword/ui/*, .../resetpassword/viewmodel/*
Added CheckPassword and ResetPassword routes, screens, and viewmodels (compare/edit password flows, validation, side effects).
Vote & misc UI updates
feature/.../vote/*, feature/.../meal/*, feature/.../home/*, feature/.../signin/*, feature/.../main/application/ui/ApplicationScreen.kt
Vote screen now initializes from result store (initState uses LocalDateTime + UUID); DmsItemButton usage switched to drawable IDs; SignIn uses DmsSymbolContent; small import/layout changes.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant SelectUI as SelectProfile Screen
    participant SelectVM as SelectProfileViewModel
    participant FileRepo as FileRepository
    participant NetworkDS as NetworkFileDataSource
    participant FileAPI as File API Service
    participant MediaStore

    User->>SelectUI: open gallery (request permission)
    SelectUI->>MediaStore: query images
    MediaStore-->>SelectUI: image URIs
    User->>SelectUI: select uri
    SelectUI->>SelectVM: selectImage(uri)
    User->>SelectUI: tap "์„ ํƒ" (upload)
    SelectUI->>SelectVM: uploadProfileImage(uri)
    SelectVM->>FileRepo: fetchPresignedUrl(fileName)
    FileRepo->>NetworkDS: fetchPresignedUrl(fileName)
    NetworkDS->>FileAPI: GET presigned URL
    FileAPI-->>NetworkDS: presigned url response
    NetworkDS-->>FileRepo: PresignedFileUrl
    FileRepo-->>SelectVM: PresignedFileUrl
    SelectVM->>FileRepo: uploadFile(presignedUrl, file)
    FileRepo->>NetworkDS: uploadFile(presignedUrl, file)
    NetworkDS->>FileAPI: PUT file to presigned URL
    FileAPI-->>NetworkDS: file upload response (fileUrl)
    NetworkDS-->>FileRepo: FileUrl
    FileRepo-->>SelectVM: FileUrl
    SelectVM-->>SelectUI: emit SuccessProfileImage (profileImageUrl)
    SelectUI->>User: navigate to AdjustProfile
Loading
sequenceDiagram
    participant User
    participant CheckUI as CheckPassword Screen
    participant CheckVM as CheckPasswordViewModel
    participant UserRepo as UserRepository
    participant UserAPI as User API

    User->>CheckUI: enter current password
    CheckUI->>CheckVM: resetPassword()
    CheckVM->>UserRepo: comparePassword(currentPassword)
    UserRepo->>UserAPI: verify endpoint
    UserAPI-->>UserRepo: success / error
    alt success
        UserRepo-->>CheckVM: success
        CheckVM-->>CheckUI: emit SuccessCheckPassword
        CheckUI->>User: navigate to ResetPassword screen
    else failure
        UserRepo-->>CheckVM: error
        CheckVM-->>CheckUI: emit FailCheckPassword(message)
        CheckUI->>User: show snackbar
    end
Loading

Estimated code review effort

๐ŸŽฏ 4 (Complex) | โฑ๏ธ ~60 minutes

Possibly related PRs

Suggested reviewers

  • parkuiery

Poem

๐Ÿฐ I hopped through new screens and stitched a tiny thread,

Images fly on presigned wings, points march ahead,
Passwords checked and settings turned with care,
Icons gleam and routes now dance everywhere,
A joyful rabbit cheers โ€” code polished, freshly fed.

๐Ÿšฅ Pre-merge checks | โœ… 2 | โŒ 1
โŒ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage โš ๏ธ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
โœ… Passed checks (2 passed)
Check name Status Explanation
Description Check โœ… Passed Check skipped - CodeRabbitโ€™s high-level summary is enabled.
Title check โœ… Passed The title '๐Ÿ”€ :: (#816) ๋งˆ์ดํŽ˜์ด์ง€ ๊ตฌํ˜„' clearly references the main feature implementation (my page) and corresponds to the primary changes in the PR.

โœ๏ธ Tip: You can configure your own custom pre-merge checks in the settings.

โœจ 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

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link

Summary of Changes

Hello @uson1004, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

์ด PR์€ ์‚ฌ์šฉ์ž์—๊ฒŒ ๊ณ„์ • ์„ค์ •์„ ๊ด€๋ฆฌํ•˜๊ณ  ๊ฐœ์ธ ์ •๋ณด๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋Š” ์ค‘์•™ ์ง‘์ค‘์‹ ํ—ˆ๋ธŒ๋ฅผ ์ œ๊ณตํ•˜๋Š” ํฌ๊ด„์ ์ธ '๋งˆ์ดํŽ˜์ด์ง€' ๊ธฐ๋Šฅ์„ ๋„์ž…ํ•ฉ๋‹ˆ๋‹ค. ์•ˆ์ „ํ•œ ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ •, ์ด๋ฏธ์ง€ ์„ ํƒ ๋ฐ ์กฐ์ • ๊ธฐ๋Šฅ์„ ํ†ตํ•œ ํ”„๋กœํ•„ ์‚ฌ์ง„ ์‚ฌ์šฉ์ž ์ •์˜, ํ‘ธ์‹œ ์•Œ๋ฆผ ํ™˜๊ฒฝ ์„ค์ • ๊ด€๋ฆฌ, ๊ทธ๋ฆฌ๊ณ  ์•ˆ์ „ํ•œ ๋กœ๊ทธ์•„์›ƒ์„ ์œ„ํ•œ ์ „์šฉ ํ™”๋ฉด๊ณผ ๋‚ด๋น„๊ฒŒ์ด์…˜ ํ๋ฆ„์„ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ, ์‚ฌ์šฉ์ž์˜ ์ƒ๋ฒŒ์  ๋‚ด์—ญ์„ ์ƒ์„ธํ•˜๊ฒŒ ๋ณด์—ฌ์ฃผ๋Š” ํƒญ ํ˜•์‹์˜ ๋ทฐ๋ฅผ ์ œ๊ณตํ•˜์—ฌ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ํ–ฅ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค.

Highlights

  • ๋งˆ์ดํŽ˜์ด์ง€ UI ๊ตฌํ˜„: ์‚ฌ์šฉ์ž ํ”„๋กœํ•„ ์ •๋ณด, ๋ฌธ๊ตฌ, ์ƒ๋ฒŒ์  ํ˜„ํ™ฉ์„ ํ‘œ์‹œํ•˜๊ณ  ์„ค์ • ๋ฐ ์ƒ๋ฒŒ์  ๋‚ด์—ญ์œผ๋กœ ์ด๋™ํ•  ์ˆ˜ ์žˆ๋Š” ๋งˆ์ดํŽ˜์ด์ง€ ํ™”๋ฉด์ด ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
  • ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ๊ธฐ๋Šฅ: ๊ธฐ์กด ๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ ํ›„ ์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋Š” ํ”Œ๋กœ์šฐ์™€ UI๊ฐ€ ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
  • ํ”„๋กœํ•„ ์‚ฌ์ง„ ๋ณ€๊ฒฝ ๊ธฐ๋Šฅ: ๊ธฐ๊ธฐ ๊ฐค๋Ÿฌ๋ฆฌ์—์„œ ์ด๋ฏธ์ง€๋ฅผ ์„ ํƒํ•˜๊ณ , ์„ ํƒ๋œ ์ด๋ฏธ์ง€๋ฅผ ์กฐ์ •(ํ™•๋Œ€/์ถ•์†Œ)ํ•˜์—ฌ ํ”„๋กœํ•„ ์‚ฌ์ง„์œผ๋กœ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์ด ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ์œ„ํ•ด READ_MEDIA_IMAGES ๊ถŒํ•œ์ด ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
  • ์„ค์ • ํ™”๋ฉด ๋ฐ ๊ธฐ๋Šฅ: ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ •, ํ”„๋กœํ•„ ์‚ฌ์ง„ ๋ณ€๊ฒฝ, ํ‘ธ์‹œ ์•Œ๋ฆผ ์„ค์ •, ๋กœ๊ทธ์•„์›ƒ ๊ธฐ๋Šฅ์„ ํฌํ•จํ•˜๋Š” ์„ค์ • ํ™”๋ฉด์ด ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋กœ๊ทธ์•„์›ƒ ์‹œ ํ™•์ธ ๋Œ€ํ™”์ƒ์ž๊ฐ€ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.
  • ์ƒ๋ฒŒ์  ๋‚ด์—ญ ํ™”๋ฉด: ์ „์ฒด, ์ƒ์ , ๋ฒŒ์  ํƒญ์œผ๋กœ ๊ตฌ๋ถ„ํ•˜์—ฌ ์‚ฌ์šฉ์ž์˜ ์ƒ๋ฒŒ์  ๋‚ด์—ญ์„ ์ƒ์„ธํ•˜๊ฒŒ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋Š” ํ™”๋ฉด์ด ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
  • ๊ณตํ†ต UI ์ปดํฌ๋„ŒํŠธ ๊ฐœ์„ : DMS ๋””์ž์ธ ์‹œ์Šคํ…œ์— AlertDialog, DmsSymbolContent (๊ธฐ์กด DmsSymbol ๊ฐœ์„ ), DmsItemButton (์•„์ด์ฝ˜ ํƒ€์ž… ๋ณ€๊ฒฝ) ๋“ฑ ์ƒˆ๋กœ์šด ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ถ”๊ฐ€๋˜๊ฑฐ๋‚˜ ๊ฐœ์„ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
  • ๋ฐ์ดํ„ฐ ๋ฐ ๋„คํŠธ์›Œํฌ ๊ณ„์ธต ํ™•์žฅ: ํŒŒ์ผ ์—…๋กœ๋“œ ๋ฐ ์‚ฌ์šฉ์ž ์ •๋ณด(๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ) ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•œ FileRepository ๋ฐ UserRepository๊ฐ€ ๋ฐ์ดํ„ฐ ๊ณ„์ธต์— ์ถ”๊ฐ€๋˜์—ˆ์œผ๋ฉฐ, ๊ด€๋ จ ๋„คํŠธ์›Œํฌ API ์„œ๋น„์Šค ๋ฐ ๋ชจ๋ธ์ด ์ •์˜๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

๐Ÿง  New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with ๐Ÿ‘ and ๐Ÿ‘Ž on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. โ†ฉ

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: 8

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Fix all issues with AI Agents ๐Ÿค–
In @app/src/dev/AndroidManifest.xml:
- Line 10: Add backward compatibility for media permissions: update
AndroidManifest.xml to include the legacy permission
android.permission.READ_EXTERNAL_STORAGE in addition to
android.permission.READ_MEDIA_IMAGES, and modify runtime permission logic in
SelectProfileScreen.kt to check Build.VERSION.SDK_INT so it requests
android.permission.READ_MEDIA_IMAGES on Android 13+ (API 33+) and
android.permission.READ_EXTERNAL_STORAGE on Android 12 and below, mirroring the
pattern used in SignInScreen.kt for notifications; ensure permission strings and
request flow are switched based on the SDK check.

In @app/src/dev/kotlin/team/aliens/dms/android/app/DmsApp.kt:
- Around line 279-287: The code reconstructs the nav key instead of using the
provided entry parameter; update the entry<AdjustProfileScreenNav> lambda to
accept a named parameter (e.g., entry) and call backStack.remove(entry) in
onBackPressed, and use entry.model for the model parameter rather than
constructing AdjustProfileScreenNav(it.model).
- Around line 190-199: The onNavigateBack lambda reconstructs a VoteScreenNav
instance and calls backStack.remove(...) which is fragile because removal relies
on exact equality; instead remove the actual entry object passed into the
entry<VoteScreenNav> { voteNav -> } lambda. Locate the entry block that creates
VoteRoute (entry<VoteScreenNav> { voteNav -> }) and change the onNavigateBack
implementation to call backStack.remove(voteNav) (use the voteNav parameter
directly) rather than creating a new VoteScreenNav instance.
- Around line 244-247: The current lambda constructs a new PointHistoryScreenNav
to remove (fragile); instead capture the nav entry provided by the entry<T>
block and remove that exact instance. Change the entry<PointHistoryScreenNav>
block to accept the nav variable (e.g., entry<PointHistoryScreenNav> { nav ->
PointHistoryRoute(onBackClick = { backStack.remove(nav) } ) }) so
PointHistoryRoute calls backStack.remove with the original nav instance rather
than recreating one.
- Around line 69-70: PointHistoryScreenNav is @Serializable but its property
PointType is not, causing runtime failures; mark the PointType enum itself with
@Serializable. Edit the PointType enum declarations (both the prod and dev
variants of the PointType enum) and add the @Serializable annotation to the enum
definition so PointHistoryScreenNav can be serialized correctly.

In
@core/design-system/src/dev/java/team/aliens/dms/android/core/designsystem/dialog/AlertDialog.kt:
- Around line 25-26: The titleContentColor and textContentColor default to
DmsTheme.colorScheme.surfaceContainer which is a background color; change their
defaults to appropriate "on" content colors (e.g.,
DmsTheme.colorScheme.onSurface or DmsTheme.colorScheme.onSurfaceVariant) so
text/heading use accessible contrast. Update the default values for
titleContentColor and textContentColor in AlertDialog.kt to use the
onSurface/onSurfaceVariant variants from DmsTheme.colorScheme.

In @data/src/dev/kotlin/team.aliens.dms.android.data/point/model/Point.kt:
- Line 3: Remove the unused import
team.aliens.dms.android.shared.date.toLocalDate from PointMapper.kt; keep the
kotlinx.datetime.LocalDate import and ensure no code references to toLocalDate
remain (update/remove any unused helper calls), then re-run the build or linter
to confirm the unused-import warning is resolved.

In
@data/src/dev/kotlin/team.aliens.dms.android.data/user/repository/UserRepository.kt:
- Around line 5-10: The dev variant API in UserRepository.kt exposes
editPassword and comparePassword returning Result<Unit>, which mismatches the
prod variant; update the signatures for both abstract suspend fun
editPassword(currentPassword: String, newPassword: String) and abstract suspend
fun comparePassword(password: String) to the same return type as the prod
variant (or vice versa) across build variants โ€” choose one consistent contract
(preferably Result<Unit> for unified error handling), then update all
implementing classes and usages to match that return type so both dev and prod
have identical method signatures.

In
@feature/src/dev/kotlin/team/aliens/dms/android/feature/main/application/ui/ApplicationScreen.kt:
- Line 7: Remove the unused import navigationBarsPadding from the top of
ApplicationScreen.kt; if you intended to use system navigation padding instead
apply navigationBarsPadding() to the relevant Modifier chain in the composable
(e.g., the root Modifier in ApplicationScreen) otherwise simply delete the
import statement to eliminate the unused import.

In
@feature/src/dev/kotlin/team/aliens/dms/android/feature/main/mypage/ui/component/OptionsContent.kt:
- Around line 31-62: OptionsContent currently creates Option items with empty
onClick lambdas; add callback parameters to the OptionsContent composable
signature (e.g., onChangePassword: () -> Unit, onOpenNotificationSettings: () ->
Unit, onThemeChange: () -> Unit, onSignOut: () -> Unit, onWithdraw: () -> Unit)
alongside the existing onNavigatePointHistory(PointType) and then replace each
empty lambda in the Option lists so "๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ" calls onChangePassword, "์•Œ๋ฆผ ์„ค์ •"
calls onOpenNotificationSettings, "ํ…Œ๋งˆ ๋ณ€๊ฒฝ" calls onThemeChange, "๋กœ๊ทธ์•„์›ƒ" calls
onSignOut, and "ํšŒ์› ํƒˆํ‡ด" calls onWithdraw (leave onNavigatePointHistory for "์ƒ๋ฒŒ์ 
๋‚ด์—ญ ํ™•์ธ" as-is).

In
@feature/src/dev/kotlin/team/aliens/dms/android/feature/main/mypage/ui/component/PointCards.kt:
- Around line 56-60: The when expression for pointType currently falls through
to else for PointType.ALL; add an explicit branch for PointType.ALL that returns
a visible label (e.g. "์ „์ฒด") and appropriate colors so it no longer relies on the
generic else case โ€” update the destructuring assignment (val (text, textColor,
backgroundColor)) to include PointType.ALL -> Triple("์ „์ฒด",
DmsTheme.colorScheme.onSurface, DmsTheme.colorScheme.surface) (or other
project-appropriate neutral colors) while keeping the existing PointType.BONUS
and PointType.MINUS branches.

In
@feature/src/dev/kotlin/team/aliens/dms/android/feature/point/ui/component/PointItem.kt:
- Around line 47-63: The destructured backgroundColor from the Triple
(backgroundColor, icon, pointColor) is unused; update the Box in PointItem.kt to
apply a background using that backgroundColor (e.g., via
Modifier.background(backgroundColor, shape) or a suitable Material/DmsTheme
shape) so the backgroundColor is actually used for the icon container; ensure
the Box keeps its contentAlignment and existing Image usage and preserve
pointColor for any text/icon tinting if needed.

In
@feature/src/dev/kotlin/team/aliens/dms/android/feature/point/ui/PointHistoryScreen.kt:
- Around line 100-106: The remember call for pointHistoryList uses an incomplete
key causing stale values: update the keys to include all dependencies (page plus
state.allPointList, state.bonusPointList, and state.minusPointList) so the
lambda recomputes when any list changes; locate the remember(...) that defines
pointHistoryList (which switches on tabData[page] with PointTab.All,
PointTab.Bonus, PointTab.Minus) and expand its key list to cover page and each
of state.allPointList, state.bonusPointList, state.minusPointList.
- Around line 40-42: The empty LaunchedEffect(Unit) block in PointHistoryScreen
(the composable in PointHistoryScreen.kt) should be removed because it has no
effect; locate the LaunchedEffect(Unit) invocation and delete the entire block
(or replace it with the intended side-effect logic if there was supposed to be
one), ensuring no dangling braces or unused imports remain after removal.

In
@feature/src/dev/kotlin/team/aliens/dms/android/feature/point/viewmodel/PointHistoryViewModel.kt:
- Line 3: Remove the unused import androidx.lifecycle.SavedStateHandle from
PointHistoryViewModel.kt; locate the import statement at the top of the file and
delete it (or let your IDE/organize imports remove it), then rebuild or run lint
to ensure no other unused imports remain.
- Line 8: Remove the unused import NotificationRepository from the top of
PointHistoryViewModel.kt; open the PointHistoryViewModel file, delete the line
"import
team.aliens.dms.android.data.notification.repository.NotificationRepository",
and run your IDE's "Optimize Imports" (or ktlint/gradle format) to ensure no
other unused imports remain.

In
@feature/src/dev/kotlin/team/aliens/dms/android/feature/profile/ui/SelectProfileScreen.kt:
- Line 63: The permission usage in SelectProfileScreen (the
rememberPermissionState call assigning request) uses
Manifest.permission.READ_MEDIA_IMAGES which exists only on API 33+; update the
permission selection to check Build.VERSION.SDK_INT >=
Build.VERSION_CODES.TIRAMISU and pass READ_MEDIA_IMAGES for API 33+, otherwise
pass Manifest.permission.READ_EXTERNAL_STORAGE for pre-33 devices, ensuring the
rememberPermissionState call (and any permission request flow) uses the chosen
permission string so older devices fall back correctly.
- Around line 71-75: The side-effect name
SelectProfileSideEffect.ProfileImageBadRequest is misleading because it triggers
a success snackbar; rename this variant to something like
ProfileImageUploadSuccess and update all usages (the when branch in
SelectProfileScreen.kt where ProfileImageBadRequest is matched, the sealed
class/enum declaration for SelectProfileSideEffect, any places that emit this
side effect in ViewModel or repository code, and any tests or handlers that
reference the old name) so the variant and its handling consistently reflect a
successful upload.

In
@feature/src/dev/kotlin/team/aliens/dms/android/feature/profile/viewmodel/AdjustProfileViewModel.kt:
- Around line 17-26: The editProfile function in AdjustProfileViewModel lacks a
loading guard and state toggling, so add a loading flag to the UI state (e.g.,
isLoading) and update editProfile to first check and return if already loading,
then set isLoading = true (and set buttonEnabled = false) before invoking
studentRepository.editProfile, and ensure you set isLoading = false (and restore
buttonEnabled appropriately) in a finally block after the runCatching so the
button is disabled during the request and re-enabled only when appropriate;
update sendEffect calls unchanged but rely on the state changes via setState to
drive the UI.

In
@feature/src/dev/kotlin/team/aliens/dms/android/feature/profile/viewmodel/SelectProfileViewModel.kt:
- Around line 86-107: In fetchPresignedUrl, you currently pass file.toString()
to uploadFile instead of getting a presigned URL; modify the implementation to
first call fileRepository.fetchPresignedUrl(file.name) (or appropriate filename
getter) inside the coroutine, then pass the obtained presignedUrl to
fileRepository.uploadFile(presignedUrl = fetchedUrl, file = file), keeping the
existing runCatching/onSuccess/onFailure flow and updating the onSuccess
handling to use the upload response (fileUrl.fileUrl) and emit the same setState
and sendEffect as before.
- Around line 30-54: getUriImage currently queries MediaStore on the main thread
and calls setState inside the cursor loop; move the MediaStore query and cursor
iteration off the main thread (use a coroutine / withContext(Dispatchers.IO) or
another background dispatcher) to avoid UI jank, accumulate uriList while
iterating the cursor on the background thread, then call setState a single time
after the loop to update the ViewModel state; keep using cursor?.use { ... }
inside the background block and reference getUriImage and setState when applying
this change.

In
@feature/src/dev/kotlin/team/aliens/dms/android/feature/resetpassword/viewmodel/CheckPasswordViewModel.kt:
- Around line 15-28: The method resetPassword() is misnamed and hides actual
error details; rename it to checkPassword() or verifyCurrentPassword() wherever
referenced (e.g., ViewModel method and any call sites) and update
documentation/tests accordingly, and change the onFailure branch to pass the
real error message from the repository into
sendEffect(CheckPasswordSideEffect.FailCheckPassword(...)) instead of the
hardcoded "๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์•„์š”" (e.g., use the Throwable.message or a mapped
user-friendly message derived from the Throwable) while keeping the existing
state updates in setState.

In
@feature/src/dev/kotlin/team/aliens/dms/android/feature/resetpassword/viewmodel/ResetPasswordViewModel.kt:
- Around line 35-43: The current code only calls
userRepository.comparePassword(...) which only validates the password; replace
or extend this flow to call userRepository.editPassword(currentPassword =
uiState.value.currentPassword, newPassword = uiState.value.newPassword) to
persist the change (or call editPassword after comparePassword succeeds), await
its result, and onSuccess setState { it.copy(isLoading = false, buttonEnabled =
true) } and sendEffect(ResetPasswordSideEffect.SuccessResetPassword), while
onFailure setState { it.copy(isLoading = false, buttonEnabled = true) } and
sendEffect(ResetPasswordSideEffect.FailResetPassword("๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ์„ ์‹คํŒจํ–ˆ์–ด์š”")) so the
UI reflects the real backend update.

In
@feature/src/dev/kotlin/team/aliens/dms/android/feature/setting/ui/SettingScreen.kt:
- Around line 120-124: The DmsItemButton for "ํ”„๋กœํ•„ ์‚ฌ์ง„ ๋ณ€๊ฒฝ" is using the calendar
icon R.drawable.img_calendar; update the iconRes on that DmsItemButton (the one
with text = "ํ”„๋กœํ•„ ์‚ฌ์ง„ ๋ณ€๊ฒฝ" and onClick = onNavigateSelectProfile) to a
profile-appropriate drawable (e.g., a profile or camera resource such as
R.drawable.img_profile or R.drawable.ic_camera) so the icon matches the action.

In
@feature/src/dev/kotlin/team/aliens/dms/android/feature/setting/viewmodel/SettingViewModel.kt:
- Around line 49-63: In fetchNotificationStatus(), avoid calling .toString() on
the nullable uiState.value.deviceToken which turns null into the literal "null"
and breaks the API; instead bail out early if deviceToken is null (or emit an
appropriate effect) before calling
notificationRepository.fetchNotificationStatus, e.g. obtain val token =
uiState.value.deviceToken ?: run { setState { it.copy(isOnNotification = false)
}; sendEffect(SettingSideEffect.CannotFetchNotificationStatus); return }, then
use token when calling notificationRepository.fetchNotificationStatus and keep
the existing success/failure handling with setState and sendEffect.
- Around line 41-47: The signOut() coroutine only handles the success branch and
ignores failures; update signOut() to handle the failure case from
authRepository.signOut() by adding an onFailure { } callback that calls
sendEffect with a new side effect (e.g., SettingSideEffect.SignOutFailure or a
variant that carries an error message/Throwable) so the UI can react; ensure you
reference the existing sendEffect function and SettingSideEffect sealed
class/enum and include any error info or a user-friendly message when emitting
the failure side effect.
- Around line 65-70: The updateNotificationStatus function's parameter and logic
are inverted: rename the parameter (e.g., currentIsOn -> previousIsOn or
desiredIsOn -> newIsOn) to reflect intent, update the setState call in
SettingViewModel.updateNotificationStatus to set isOnNotification to the new
desired value (do not unconditionally negate the incoming parameter), and invert
the conditional so that when turning notifications on you perform the subscribe
flow and when turning off you perform the unsubscribe flow by calling
setNotificationStatus appropriately (adjust setNotificationStatus to accept an
explicit desired state or add separate subscribe/unsubscribe helpers) instead of
the current if (isOnNotification) ... else return pattern.

In @gradle/libs.versions.toml:
- Line 5: Update the library versions in gradle/libs.versions.toml: change the
accompanist entry from "0.36.0" to "0.37.3" and update the telephoto entry from
"0.14.0" to "0.18.0" so the project uses the latest stable releases; locate and
modify the accompanist and telephoto keys in the TOML file and ensure any
dependency usages referencing those version variables continue to resolve
correctly (adjust lockfiles or run a Gradle sync/build to apply the changes).

In
@network/src/dev/kotlin/team.aliens.dms.android.network/file/datasource/NetworkFileDataSource.kt:
- Line 9: The prod flavor still uses the old signature returning Unit; update
the abstract declaration abstract suspend fun uploadFile(...) in
NetworkFileDataSource to return FetchFileUrlResponse (matching the dev flavor)
and change the implementation NetworkFileDataSourceImpl.uploadFile to have an
explicit return type of FetchFileUrlResponse and return the appropriate
FetchFileUrlResponse value from the method body so both abstract and concrete
match.

In
@network/src/dev/kotlin/team.aliens.dms.android.network/file/model/FetchFileUrlResponse.kt:
- Around line 5-7: The model FetchFileUrlResponse assumes fileUrl is always
present and non-null which will cause Gson deserialization to throw and crash if
the API omits or nulls "file_url"; either make the property defensive (e.g.,
change FetchFileUrlResponse.fileUrl to a nullable type or provide a safe default
and validate empty strings) and enforce validation where the response is
consumed, or update the network error handling in handleNetworkRequest to catch
JSON parse errors (e.g., JsonSyntaxException/JsonIOException) and surface a
clear error instead of letting the app crash; reference
FetchFileUrlResponse.fileUrl and handleNetworkRequest when implementing these
changes.

Caution

Some comments are outside the diff and canโ€™t be posted inline due to platform limitations.

โš ๏ธ Outside diff range comments (1)
feature/build.gradle.kts (1)

74-96: Update telephoto-zoomable-image-coil to latest version and consolidate shared dependencies.

  1. telephoto-zoomable-image-coil (line 96): Currently v0.14.0, but v0.18.0 (released Oct 4, 2025) is available. Update to the latest version.

  2. kotlinx.datetime (line 94): Duplication detectedโ€”already declared in data/build.gradle.kts. Move shared dependencies to a higher-level configuration (app or shared build file) rather than duplicating across modules.

  3. accompanist.permissions (line 95): No single official Compose replacement exists. If migrating away from Accompanist, use Activity Result APIs with rememberLauncherForActivityResult and ActivityResultContracts instead.

๐ŸŸ  Major comments (18)
gradle/libs.versions.toml-5-5 (1)

5-5: Update accompanist and telephoto to their latest versions.

  • accompanist should be updated from 0.36.0 (Sep 2024) to 0.37.3 (Apr 2025)
  • telephoto should be updated from 0.14.0 to 0.18.0

The library selections are appropriate for the MyPage profile image features, but the versions are outdated. Update to the latest stable versions to ensure you have the latest bug fixes, security patches, and features.

feature/src/dev/kotlin/team/aliens/dms/android/feature/profile/viewmodel/AdjustProfileViewModel.kt-17-26 (1)

17-26: Add loading state to prevent duplicate submissions.

The function doesn't track loading state, allowing users to tap the button multiple times before the API call completes, potentially causing duplicate profile update requests.

๐Ÿ”Ž Proposed fix to add loading state
 internal fun editProfile(profileImageUrl: String) = viewModelScope.launch(Dispatchers.IO) {
+    setState { it.copy(buttonEnabled = false) }
     runCatching {
         studentRepository.editProfile(profileImageUrl)
     }.onSuccess {
         sendEffect(AdjustProfileSideEffect.ProfileImageSet)
-        setState { it.copy(buttonEnabled = false) }
     }.onFailure {
         sendEffect(AdjustProfileSideEffect.ProfileImageBadRequest)
+        setState { it.copy(buttonEnabled = true) }
     }
 }
feature/src/dev/kotlin/team/aliens/dms/android/feature/main/mypage/viewmodel/MyPageViewModel.kt-17-28 (1)

17-28: Add error handling and loading state management.

The current implementation has two reliability concerns that degrade user experience:

  1. Empty error handling (lines 25-26): The onFailure block silently swallows all errors without logging, user notification, or state updates. Users won't receive feedback when data fetching fails.

  2. Missing loading state: The UI cannot display loading indicators because MyPageState doesn't track the loading status.

Additionally, consider moving Dispatchers.IO to the repository layer, as ViewModels should remain dispatcher-agnostic and let the data layer handle threading.

๐Ÿ”Ž Proposed improvements

First, update MyPageState to include loading and error states:

 internal data class MyPageState(
     val myPage: MyPage = MyPage(),
+    val isLoading: Boolean = false,
+    val error: String? = null,
 )

Then update the getMyPage method to properly manage state:

 private fun getMyPage() {
+    setState { it.copy(isLoading = true, error = null) }
-    viewModelScope.launch(Dispatchers.IO) {
+    viewModelScope.launch {
         studentRepository.fetchMyPage().onSuccess { myPage ->
-            setState { it.copy(myPage = myPage) }
+            setState { it.copy(myPage = myPage, isLoading = false) }
         }.onFailure {
+            setState { it.copy(isLoading = false, error = it.message) }
         }
     }
 }

Ensure the repository handles Dispatchers.IO internally for its suspend functions.

Committable suggestion skipped: line range outside the PR's diff.

feature/src/dev/kotlin/team/aliens/dms/android/feature/setting/viewmodel/SettingViewModel.kt-41-47 (1)

41-47: Add error handling for sign-out failures.

The signOut() function only handles the success case. If the sign-out operation fails, no side effect is emitted, leaving the UI unaware of the failure. This could result in a poor user experience where the app appears unresponsive or stuck.

๐Ÿ”Ž Proposed fix
     internal fun signOut() {
         viewModelScope.launch(Dispatchers.IO) {
             authRepository.signOut().onSuccess {
                 sendEffect(SettingSideEffect.SignOutSuccess)
+            }.onFailure {
+                sendEffect(SettingSideEffect.SignOutFailure)
             }
         }
     }

Then add the corresponding side effect:

 sealed class SettingSideEffect {
     object CannotFetchNotificationStatus : SettingSideEffect()
     object SignOutSuccess : SettingSideEffect()
+    object SignOutFailure : SettingSideEffect()
 }

Committable suggestion skipped: line range outside the PR's diff.

feature/src/dev/kotlin/team/aliens/dms/android/feature/setting/viewmodel/SettingViewModel.kt-65-70 (1)

65-70: Confusing logic and parameter naming in notification status update.

The function has several issues that make the logic hard to follow:

  1. Parameter naming: isOnNotification suggests it's the new desired state, but the logic treats it as the current state.
  2. State update on line 67: Negates the current value regardless of intended behavior.
  3. Line 69: The conditional logic is inverted and unclearโ€”if (isOnNotification) triggers unsubscribe, but then else return prevents subscription when turning notifications on.

This likely results in incorrect behavior when toggling notification status.

๐Ÿ”Ž Proposed fix

Clarify the parameter name and fix the logic:

-    internal fun updateNotificationStatus(isOnNotification: Boolean) {
+    internal fun updateNotificationStatus(currentIsOn: Boolean) {
         setState { settingState ->
-            settingState.copy(isOnNotification = !isOnNotification)
+            settingState.copy(isOnNotification = !currentIsOn)
         }
-        if (isOnNotification) /* ๊ตฌ๋… ์ทจ์†Œ */ setNotificationStatus() else return // TODO ๊ตฌ๋… ์—…๋ฐ์ดํŠธ (false -> true)
+        if (currentIsOn) {
+            // ๊ตฌ๋… ์ทจ์†Œ (currently on โ†’ turning off)
+            setNotificationStatus(shouldSubscribe = false)
+        } else {
+            // ๊ตฌ๋… ํ™œ์„ฑํ™” (currently off โ†’ turning on)
+            setNotificationStatus(shouldSubscribe = true) // TODO: Implement subscription
+        }
     }

Update the placeholder function signature accordingly:

-    private fun setNotificationStatus() {
+    private fun setNotificationStatus(shouldSubscribe: Boolean) {
+        // TODO: Implement subscription/unsubscription logic
     }

Committable suggestion skipped: line range outside the PR's diff.

feature/src/dev/kotlin/team/aliens/dms/android/feature/profile/viewmodel/SelectProfileViewModel.kt-30-54 (1)

30-54: Performance: Move MediaStore query off main thread and batch state update.

Two issues:

  1. Main thread blocking: The MediaStore query runs on the main thread during init, which can cause UI jank.
  2. Inefficient state updates: setState is called inside the loop (line 51), triggering multiple state updates. Call it once after the loop.
๐Ÿ”Ž Proposed fix
-    private fun getUriImage() {
-        val uriList = mutableListOf<String>()
-        val contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
-        val projection = arrayOf(MediaStore.Images.Media._ID)
-
-
-        // contentResolver๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์™ธ๋ถ€ ์ €์žฅ์†Œ์—์„œ ์ด๋ฏธ์ง€ ๋ฐ์ดํ„ฐ ์ฟผ๋ฆฌ
-        val cursor = context.contentResolver.query(
-            contentUri,
-            projection,
-            null,
-            null,
-            "${MediaStore.Images.Media.DATE_ADDED} DESC"
-        )
-
-        cursor?.use {
-            val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
-            while (cursor.moveToNext()) {
-                val id = cursor.getLong(idColumn)
-                val imageUri = ContentUris.withAppendedId(contentUri, id)
-                uriList.add(imageUri.toString())
-                setState { it.copy(uriList = uriList) }
+    private fun getUriImage() {
+        viewModelScope.launch(Dispatchers.IO) {
+            val uriList = mutableListOf<String>()
+            val contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
+            val projection = arrayOf(MediaStore.Images.Media._ID)
+
+            val cursor = context.contentResolver.query(
+                contentUri,
+                projection,
+                null,
+                null,
+                "${MediaStore.Images.Media.DATE_ADDED} DESC"
+            )
+
+            cursor?.use {
+                val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
+                while (cursor.moveToNext()) {
+                    val id = cursor.getLong(idColumn)
+                    val imageUri = ContentUris.withAppendedId(contentUri, id)
+                    uriList.add(imageUri.toString())
+                }
             }
+            setState { it.copy(uriList = uriList) }
         }
     }
feature/src/dev/kotlin/team/aliens/dms/android/feature/point/viewmodel/PointHistoryViewModel.kt-3-3 (1)

3-3: Remove unused import.

SavedStateHandle is imported but not used anywhere in the file.

๐Ÿ”Ž Proposed fix
-import androidx.lifecycle.SavedStateHandle
 import androidx.lifecycle.viewModelScope
feature/src/dev/kotlin/team/aliens/dms/android/feature/point/viewmodel/PointHistoryViewModel.kt-8-8 (1)

8-8: Remove unused import.

NotificationRepository is imported but not used anywhere in the file.

๐Ÿ”Ž Proposed fix
 import team.aliens.dms.android.core.ui.viewmodel.BaseStateViewModel
-import team.aliens.dms.android.data.notification.repository.NotificationRepository
 import team.aliens.dms.android.data.point.model.Point
app/src/dev/AndroidManifest.xml-10-10 (1)

10-10: Add backward compatibility for READ_EXTERNAL_STORAGE on Android 12 and below.

The runtime permission request for READ_MEDIA_IMAGES is properly implemented in SelectProfileScreen.kt using the Accompanist library. However, the code lacks backward compatibility handling. The READ_MEDIA_IMAGES permission was introduced in Android 13 (API 33) and does not exist on lower versions.

To fix this:

  1. Check Build.VERSION.SDK_INT before requesting permissions
  2. Request READ_MEDIA_IMAGES for Android 13+ and READ_EXTERNAL_STORAGE for Android 12 and below
  3. Add READ_EXTERNAL_STORAGE to AndroidManifest.xml for devices running older Android versions

The codebase already uses this pattern elsewhere (e.g., in SignInScreen.kt for notifications), so follow the same approach for media permissions.

feature/src/dev/kotlin/team/aliens/dms/android/feature/profile/ui/SelectProfileScreen.kt-63-63 (1)

63-63: Permission compatibility issue: READ_MEDIA_IMAGES requires API 33+.

Manifest.permission.READ_MEDIA_IMAGES is only available on Android 13 (API 33) and above. On older devices, this will fail silently or crash. Consider handling the API level check to fall back to READ_EXTERNAL_STORAGE for pre-33 devices.

๐Ÿ”Ž Proposed fix
+import android.os.Build
+
 @OptIn(ExperimentalPermissionsApi::class)
 @Composable
 internal fun SelectProfile(
     onBackPressed: () -> Unit,
     onNavigateAdjustProfile: (String) -> Unit,
     onShowSnackBar: (DmsSnackBarType, String) -> Unit,
 ) {
-    val request = rememberPermissionState(permission = Manifest.permission.READ_MEDIA_IMAGES)
+    val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+        Manifest.permission.READ_MEDIA_IMAGES
+    } else {
+        Manifest.permission.READ_EXTERNAL_STORAGE
+    }
+    val request = rememberPermissionState(permission = permission)
feature/src/dev/kotlin/team/aliens/dms/android/feature/main/mypage/ui/component/PointCards.kt-56-60 (1)

56-60: Handle PointType.ALL explicitly in the when expression.

The PointType enum includes ALL, BONUS, and MINUS. Currently, ALL falls into the else branch, which returns empty text and default colors that may cause UI issues. Add explicit handling for ALL with appropriate text and color styling.

    val (text, textColor, backgroundColor) = when (pointType) {
        PointType.BONUS -> Triple("์ƒ์ ", DmsTheme.colorScheme.secondary, DmsTheme.colorScheme.primary)
        PointType.MINUS -> Triple("๋ฒŒ์ ", DmsTheme.colorScheme.onErrorContainer, DmsTheme.colorScheme.error)
        else -> Triple("", DmsTheme.colorScheme.onSurface, DmsTheme.colorScheme.surface)
    }
app/src/dev/kotlin/team/aliens/dms/android/app/DmsApp.kt-279-287 (1)

279-287: Same fragile pattern for AdjustProfileScreenNav.

Use the entry parameter directly instead of reconstructing the nav key.

๐Ÿ”Ž Proposed fix
                         entry<AdjustProfileScreenNav> {
                             AdjustProfileRoute(
-                                onBackPressed = { backStack.remove(AdjustProfileScreenNav(it.model)) },
+                                onBackPressed = { backStack.remove(it) },
                                 model = it.model,
                                 onShowSnackBar = { snackBar, message ->
                                     appState.showSnackBar(snackBar, message)
                                 }
                             )
                         }
feature/src/dev/kotlin/team/aliens/dms/android/feature/point/ui/PointHistoryScreen.kt-100-106 (1)

100-106: Incorrect remember keys cause stale data.

The remember block uses state.allPointList as the only key, but the computation also depends on state.bonusPointList and state.minusPointList. When these lists update independently, the cached value won't recompute.

๐Ÿ”Ž Proposed fix
-                val pointHistoryList = remember(page, state.allPointList) {
+                val pointHistoryList = remember(page, state.allPointList, state.bonusPointList, state.minusPointList) {
                     when (tabData[page]) {
                         PointTab.All -> state.allPointList
                         PointTab.Bonus -> state.bonusPointList
                         PointTab.Minus -> state.minusPointList
                     }
                 }
feature/src/dev/kotlin/team/aliens/dms/android/feature/point/ui/PointHistoryScreen.kt-40-42 (1)

40-42: Remove empty LaunchedEffect block.

This LaunchedEffect has an empty body and serves no purpose. Remove it or add the intended logic.

๐Ÿ”Ž Proposed fix
-    LaunchedEffect(Unit) {
-
-    }
-
     PointHistoryScreen(
app/src/dev/kotlin/team/aliens/dms/android/app/DmsApp.kt-190-199 (1)

190-199: Fragile back navigation with reconstructed keys.

Creating a new VoteScreenNav instance for removal relies on exact equality. Use the entry parameter voteNav directly:

๐Ÿ”Ž Proposed fix
                         entry<VoteScreenNav> { voteNav ->
                             VoteRoute(
                                 title = voteNav.title,
                                 startTime = voteNav.startTime,
                                 endTime = voteNav.endTime,
-                                onNavigateBack = { backStack.remove(VoteScreenNav(voteNav.title, voteNav.startTime, voteNav.endTime)) },
+                                onNavigateBack = { backStack.remove(voteNav) },
                                 onShowSnackBar = { snackBarType, message ->
                                     appState.showSnackBar(snackBarType, message)
                                 }
                             )
                         }
app/src/dev/kotlin/team/aliens/dms/android/app/DmsApp.kt-244-247 (1)

244-247: Same fragile pattern for PointHistoryScreenNav.

๐Ÿ”Ž Proposed fix
                         entry<PointHistoryScreenNav> {
                             PointHistoryRoute(
-                                onBackClick = { backStack.remove(PointHistoryScreenNav(it.pointType)) }
+                                onBackClick = { backStack.remove(it) }
                             )
                         }
app/src/dev/kotlin/team/aliens/dms/android/app/DmsApp.kt-69-70 (1)

69-70: Ensure PointType is marked as @Serializable.

PointHistoryScreenNav is annotated with @Serializable but contains a pointType property of type PointType, which is not serializable. This will cause serialization failures at runtime. Add @Serializable to the PointType enum definition in data/src/prod/kotlin/team/aliens/dms/android/data/point/model/PointType.kt and data/src/dev/kotlin/team.aliens.dms.android.data/point/model/PointType.kt.

feature/src/dev/kotlin/team/aliens/dms/android/feature/resetpassword/viewmodel/CheckPasswordViewModel.kt-15-28 (1)

15-28: Function name resetPassword() is misleading.

The function verifies the current password by calling comparePassword, but doesn't actually reset anything. This creates confusion about the function's purpose. Consider renaming to checkPassword() or verifyCurrentPassword() to accurately reflect its behavior.

Additionally, Line 25 hardcodes the error message "๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์•„์š”", which discards the actual error from the repository. If the repository fails due to network errors, server issues, or other reasons, the user will still see "password doesn't match," which is misleading.

๐Ÿ”Ž Proposed fixes
-    internal fun resetPassword() {
+    internal fun checkPassword() {
         viewModelScope.launch {
             setState { it.copy(isLoading = true, buttonEnabled = false) }
             userRepository.comparePassword(
                 password = uiState.value.currentPassword,
             ).onSuccess {
                 setState { it.copy(isLoading = false, buttonEnabled = true) }
                 sendEffect(CheckPasswordSideEffect.SuccessCheckPassword)
             }.onFailure {
                 setState { it.copy(isLoading = false, buttonEnabled = true) }
-                sendEffect(CheckPasswordSideEffect.FailCheckPassword("๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์•„์š”"))
+                sendEffect(CheckPasswordSideEffect.FailCheckPassword(
+                    it.message ?: "๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค"
+                ))
             }
         }
     }
๐ŸŸก Minor comments (6)
feature/src/dev/kotlin/team/aliens/dms/android/feature/main/application/ui/ApplicationScreen.kt-7-7 (1)

7-7: Remove unused import.

The navigationBarsPadding import is declared but never used in this file. Remove it unless you intended to apply the padding to one of the Modifier chains.

network/src/dev/kotlin/team.aliens.dms.android.network/file/model/FetchFileUrlResponse.kt-5-7 (1)

5-7: Verify that the API consistently returns file_url in successful responses and consider defensive deserialization handling.

The data class structure is clean and follows Kotlin conventions. The non-nullable fileUrl property assumes the API always provides this field in successful responses. However, handleNetworkRequest only catches HTTP exceptions and will not catch JSON deserialization failuresโ€”if file_url is missing or null in the response, Gson will throw an exception that crashes the app.

Ensure the API contract guarantees file_url is always present and non-null in successful responses. Additionally, consider whether empty strings should be validated, or add defensive error handling for deserialization failures if the API contract cannot be guaranteed.

data/src/dev/kotlin/team.aliens.dms.android.data/point/model/Point.kt-3-3 (1)

3-3: Clean up unused import in PointMapper.kt.

The import change from java.time.LocalDate to kotlinx.datetime.LocalDate is properly implemented. The mapper correctly parses the String date from FetchPointsResponse using LocalDate.parse(), and the feature layer (PointItem.kt) handles formatting with kotlinx.datetime.format utilities. However, line 9 imports team.aliens.dms.android.shared.date.toLocalDate which is unusedโ€”remove it.

feature/src/dev/kotlin/team/aliens/dms/android/feature/profile/ui/SelectProfileScreen.kt-71-75 (1)

71-75: Confusing side effect naming: ProfileImageBadRequest shows success message.

SelectProfileSideEffect.ProfileImageBadRequest triggers a success snackbar ("์—…๋กœ๋“œ ์„ฑ๊ณต!"). The naming is misleadingโ€”consider renaming to something like ProfileImageUploadSuccess for clarity.

feature/src/dev/kotlin/team/aliens/dms/android/feature/point/ui/component/PointItem.kt-47-63 (1)

47-63: Unused backgroundColor variable.

The backgroundColor is destructured from the Triple but never used. The Box at lines 56-63 lacks a background modifier. Either apply the background or remove the unused variable.

๐Ÿ”Ž Proposed fix: apply background to the Box
         Box(
+            modifier = Modifier
+                .size(40.dp)
+                .clip(CircleShape)
+                .background(backgroundColor),
             contentAlignment = Alignment.Center,
         ) {
             Image(
                 painter = painterResource(id = icon),
                 contentDescription = null,
             )
         }
feature/src/dev/kotlin/team/aliens/dms/android/feature/setting/ui/SettingScreen.kt-120-124 (1)

120-124: Mismatched icon for profile picture change.

R.drawable.img_calendar (calendar icon) is used for "ํ”„๋กœํ•„ ์‚ฌ์ง„ ๋ณ€๊ฒฝ" (profile picture change). This seems unintentionalโ€”consider using a profile or camera-related icon.

Comment on lines +25 to +26
titleContentColor: Color = DmsTheme.colorScheme.surfaceContainer,
textContentColor: Color = DmsTheme.colorScheme.surfaceContainer,
Copy link

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue | ๐Ÿ”ด Critical

Fix incorrect content color values.

surfaceContainer is a surface/background color, not a content color. Using it for titleContentColor and textContentColor will likely result in poor contrast or invisible text. Material3 content colors should use "on" variants like onSurface or onSurfaceVariant.

๐Ÿ”Ž Proposed fix
-    titleContentColor: Color = DmsTheme.colorScheme.surfaceContainer,
-    textContentColor: Color = DmsTheme.colorScheme.surfaceContainer,
+    titleContentColor: Color = DmsTheme.colorScheme.onSurface,
+    textContentColor: Color = DmsTheme.colorScheme.onSurfaceVariant,
๐Ÿ“ 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
titleContentColor: Color = DmsTheme.colorScheme.surfaceContainer,
textContentColor: Color = DmsTheme.colorScheme.surfaceContainer,
titleContentColor: Color = DmsTheme.colorScheme.onSurface,
textContentColor: Color = DmsTheme.colorScheme.onSurfaceVariant,
๐Ÿค– Prompt for AI Agents
In
@core/design-system/src/dev/java/team/aliens/dms/android/core/designsystem/dialog/AlertDialog.kt
around lines 25-26, The titleContentColor and textContentColor default to
DmsTheme.colorScheme.surfaceContainer which is a background color; change their
defaults to appropriate "on" content colors (e.g.,
DmsTheme.colorScheme.onSurface or DmsTheme.colorScheme.onSurfaceVariant) so
text/heading use accessible contrast. Update the default values for
titleContentColor and textContentColor in AlertDialog.kt to use the
onSurface/onSurfaceVariant variants from DmsTheme.colorScheme.

abstract class NetworkFileDataSource {
abstract suspend fun fetchPresignedUrl(fileName: String): FetchPresignedUrlResponse
abstract suspend fun uploadFile(presignedUrl: String, file: File)
abstract suspend fun uploadFile(presignedUrl: String, file: File): FetchFileUrlResponse
Copy link

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue | ๐Ÿ”ด Critical

๐Ÿงฉ Analysis chain

๐Ÿ Script executed:

#!/bin/bash
# Find all implementations of NetworkFileDataSource

echo "=== Searching for NetworkFileDataSource implementations ==="
rg -n 'class.*:\s*NetworkFileDataSource' --type=kt

echo -e "\n=== Looking for uploadFile implementations ==="
rg -n 'fun uploadFile' --type=kt -B2 -A2

echo -e "\n=== Checking the abstract class definition ==="
find . -name "NetworkFileDataSource.kt" -type f | head -5 | xargs -I {} cat -n {}

Repository: team-aliens/DMS-Android

Length of output: 1225


๐Ÿ Script executed:

#!/bin/bash
# Find all implementations of NetworkFileDataSource without type filter

echo "=== Searching for all classes that implement/extend NetworkFileDataSource ==="
rg 'class.*NetworkFileDataSource' -g '*.kt'

echo -e "\n=== Finding all uploadFile implementations ==="
rg 'fun uploadFile' -g '*.kt' -B3 -A5

echo -e "\n=== List all .kt files in the file/datasource directory ==="
find . -path '*/file/datasource/*.kt' -type f

echo -e "\n=== Search for any class extending NetworkFileDataSource ==="
rg ': NetworkFileDataSource' -g '*.kt' -B2 -A10

Repository: team-aliens/DMS-Android

Length of output: 17742


๐Ÿ Script executed:

#!/bin/bash
# View the complete prod NetworkFileDataSourceImpl to see the full uploadFile signature

echo "=== Prod NetworkFileDataSourceImpl.kt complete content ==="
cat -n network/src/prod/kotlin/team.aliens.dms.android.network/file/datasource/NetworkFileDataSourceImpl.kt

echo -e "\n=== Dev NetworkFileDataSourceImpl.kt complete content ==="
cat -n network/src/dev/kotlin/team.aliens.dms.android.network/file/datasource/NetworkFileDataSourceImpl.kt

Repository: team-aliens/DMS-Android

Length of output: 3320


Update the prod flavor's NetworkFileDataSource to match the new return type.

The concern is valid. While the dev flavor has been updated to return FetchFileUrlResponse, the prod flavor's abstract class and implementation still use the old signature returning Unit. Both the abstract class definition and NetworkFileDataSourceImpl in the prod flavor must be updated to match:

  • network/src/prod/kotlin/team.aliens.dms.android.network/file/datasource/NetworkFileDataSource.kt line 8: Change signature to return FetchFileUrlResponse
  • network/src/prod/kotlin/team.aliens.dms.android.network/file/datasource/NetworkFileDataSourceImpl.kt line 22-23: Add explicit return type FetchFileUrlResponse

This inconsistency between build flavors will cause compilation or type-mismatch issues.

๐Ÿค– Prompt for AI Agents
In
@network/src/dev/kotlin/team.aliens.dms.android.network/file/datasource/NetworkFileDataSource.kt
around line 9, The prod flavor still uses the old signature returning Unit;
update the abstract declaration abstract suspend fun uploadFile(...) in
NetworkFileDataSource to return FetchFileUrlResponse (matching the dev flavor)
and change the implementation NetworkFileDataSourceImpl.uploadFile to have an
explicit return type of FetchFileUrlResponse and return the appropriate
FetchFileUrlResponse value from the method body so both abstract and concrete
match.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

๋งˆ์ดํŽ˜์ด์ง€ ๊ตฌํ˜„์„ ์œ„ํ•œ ๋Œ€๊ทœ๋ชจ ์—…๋ฐ์ดํŠธ ์ž˜ ๋ณด์•˜์Šต๋‹ˆ๋‹ค. ์ „๋ฐ˜์ ์œผ๋กœ ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ๋“ค์ด ์ž˜ ํ†ตํ•ฉ๋˜๊ณ  ์žˆ๋Š” ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค. ๋ช‡ ๊ฐ€์ง€ ์น˜๋ช…์ ์ธ ๋ฒ„๊ทธ์™€ ๊ฐœ์„ ์ ์„ ๋ฐœ๊ฒฌํ•˜์—ฌ ๋ฆฌ๋ทฐ์— ๋‚จ๊ฒผ์Šต๋‹ˆ๋‹ค. ํŠนํžˆ ํŒŒ์ผ ์—…๋กœ๋“œ ๋กœ์ง๊ณผ ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ๋กœ์ง์— ์น˜๋ช…์ ์ธ ์˜ค๋ฅ˜๊ฐ€ ์žˆ์œผ๋‹ˆ ๊ผญ ํ™•์ธ ๋ถ€ํƒ๋“œ๋ฆฝ๋‹ˆ๋‹ค. ๋˜ํ•œ, ์•ˆ๋“œ๋กœ์ด๋“œ ํ•˜์œ„ ๋ฒ„์ „ ํ˜ธํ™˜์„ฑ์„ ์œ„ํ•œ ๊ถŒํ•œ ์ฒ˜๋ฆฌ์™€ ์ผ๋ถ€ ํ™”๋ฉด์˜ ๋„ค๋น„๊ฒŒ์ด์…˜ ๋ฐ” ํŒจ๋”ฉ ๋ฌธ์ œ๋„ ์ˆ˜์ •์ด ํ•„์š”ํ•ด ๋ณด์ž…๋‹ˆ๋‹ค. ์ถ”๊ฐ€์ ์œผ๋กœ ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” ์ปดํฌ๋„ŒํŠธ๋“ค์ด ์žˆ์œผ๋‹ˆ ํ™•์ธ ํ›„ ์ •๋ฆฌํ•ด์ฃผ์‹œ๋ฉด ์ข‹๊ฒ ์Šต๋‹ˆ๋‹ค.

Comment on lines 86 to 107
internal fun fetchPresignedUrl(
file: File,
) {
viewModelScope.launch(Dispatchers.IO) {
runCatching {
fileRepository.uploadFile(
presignedUrl = file.toString(),
file = file,
)
}.onSuccess { fileUrl ->
setState {
it.copy(
profileImageUrl = fileUrl.fileUrl,
buttonEnabled = true,
)
}
sendEffect(SelectProfileSideEffect.SuccessProfileImage(fileUrl.fileUrl))
}.onFailure {
sendEffect(SelectProfileSideEffect.FailProfileImage)
}
}
}

Choose a reason for hiding this comment

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

critical

ํ”„๋กœํ•„ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ๋กœ์ง์— ์‹ฌ๊ฐํ•œ ์˜ค๋ฅ˜๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. fileRepository.uploadFile์„ ํ˜ธ์ถœํ•  ๋•Œ presignedUrl ํŒŒ๋ผ๋ฏธํ„ฐ์— file.toString()์„ ์ „๋‹ฌํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. file.toString()์€ ํŒŒ์ผ ๊ฒฝ๋กœ ๋ฌธ์ž์—ด์ผ ๋ฟ, ์œ ํšจํ•œ ์—…๋กœ๋“œ URL์ด ์•„๋‹™๋‹ˆ๋‹ค. ๋จผ์ € fileRepository.fetchPresignedUrl()์„ ํ˜ธ์ถœํ•˜์—ฌ ์—…๋กœ๋“œ์— ํ•„์š”ํ•œ Presigned URL์„ ๋ฐ›์•„์˜จ ํ›„, ๊ทธ URL์„ ์‚ฌ์šฉํ•˜์—ฌ uploadFile์„ ํ˜ธ์ถœํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ, ํ˜„์žฌ fetchPresignedUrl์ด๋ผ๋Š” ํ•จ์ˆ˜๋ช…์€ ์‹ค์ œ๋กœ๋Š” ํŒŒ์ผ ์—…๋กœ๋“œ๋ฅผ ์‹œ๋„ํ•˜๊ณ  ์žˆ์–ด ํ˜ผ๋ž€์„ ์ค๋‹ˆ๋‹ค. ํ•จ์ˆ˜๋ช…์„ uploadFile ๋“ฑ์œผ๋กœ ๋ช…ํ™•ํ•˜๊ฒŒ ๋ณ€๊ฒฝํ•˜๊ณ , ๋‚ด๋ถ€ ๋กœ์ง์„ ์ˆ˜์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    internal fun uploadFileAndGetUrl(
        file: File,
    ) {
        viewModelScope.launch(Dispatchers.IO) {
            runCatching {
                val presignedInfo = fileRepository.fetchPresignedUrl(file.name)
                fileRepository.uploadFile(
                    presignedUrl = presignedInfo.fileUploadUrl,
                    file = file,
                )
                presignedInfo.fileUrl
            }.onSuccess { fileUrl ->
                setState {
                    it.copy(
                        profileImageUrl = fileUrl,
                        buttonEnabled = true,
                    )
                }
                sendEffect(SelectProfileSideEffect.SuccessProfileImage(fileUrl))
            }.onFailure {
                sendEffect(SelectProfileSideEffect.FailProfileImage)
            }
        }
    }

Comment on lines 32 to 45
internal fun resetPassword() {
viewModelScope.launch {
setState { it.copy(isLoading = true, buttonEnabled = false) }
userRepository.comparePassword(
password = uiState.value.newPassword,
).onSuccess {
setState { it.copy(isLoading = false, buttonEnabled = true) }
sendEffect(ResetPasswordSideEffect.SuccessResetPassword)
}.onFailure {
setState { it.copy(isLoading = false, buttonEnabled = true) }
sendEffect(ResetPasswordSideEffect.FailResetPassword("๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ์„ ์‹คํŒจํ–ˆ์–ด์š”"))
}
}
}

Choose a reason for hiding this comment

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

critical

resetPassword ํ•จ์ˆ˜์—์„œ userRepository.comparePassword๋ฅผ ํ˜ธ์ถœํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ํ™”๋ฉด์€ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ '์žฌ์„ค์ •'ํ•˜๋Š” ํ™”๋ฉด์ด๋ฏ€๋กœ, comparePassword๊ฐ€ ์•„๋‹Œ editPassword์™€ ๊ฐ™์€ ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ API๋ฅผ ํ˜ธ์ถœํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ ๋กœ์ง์€ ์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ๊ธฐ์กด ๋น„๋ฐ€๋ฒˆํ˜ธ์™€ ๋น„๊ตํ•˜๋Š” ๊ฒƒ์œผ๋กœ ๋ณด์—ฌ, ๊ธฐ๋Šฅ ๋ช…์„ธ์™€ ๋งž์ง€ ์•Š์Šต๋‹ˆ๋‹ค. editPassword๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ์ˆ˜์ •ํ•ด์ฃผ์„ธ์š”.

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />

Choose a reason for hiding this comment

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

high

READ_MEDIA_IMAGES ๊ถŒํ•œ์€ API 33 ์ด์ƒ์—์„œ๋งŒ ์œ ํšจํ•ฉ๋‹ˆ๋‹ค. minSdk๊ฐ€ 26์œผ๋กœ ์„ค์ •๋˜์–ด ์žˆ์œผ๋ฏ€๋กœ, ํ•˜์œ„ ๋ฒ„์ „(API 32 ์ดํ•˜)์„ ์ง€์›ํ•˜๊ธฐ ์œ„ํ•ด READ_EXTERNAL_STORAGE ๊ถŒํ•œ๋„ ์ถ”๊ฐ€ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. maxSdkVersion="32" ์†์„ฑ๊ณผ ํ•จ๊ป˜ ์ถ”๊ฐ€ํ•˜์—ฌ ๋ฒ„์ „๋ณ„๋กœ ์˜ฌ๋ฐ”๋ฅธ ๊ถŒํ•œ์ด ์‚ฌ์šฉ๋˜๋„๋ก ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค. ๋˜ํ•œ, SelectProfileScreen.kt์—์„œ ๊ถŒํ•œ์„ ์š”์ฒญํ•  ๋•Œ๋„ Build.VERSION.SDK_INT์— ๋”ฐ๋ผ ๋ถ„๊ธฐ ์ฒ˜๋ฆฌ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

Suggested change
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />

Comment on lines +25 to +26
titleContentColor: Color = DmsTheme.colorScheme.surfaceContainer,
textContentColor: Color = DmsTheme.colorScheme.surfaceContainer,

Choose a reason for hiding this comment

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

high

AlertDialog์˜ titleContentColor์™€ textContentColor์˜ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ DmsTheme.colorScheme.surfaceContainer๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. surfaceContainer๋Š” ๋ฐฐ๊ฒฝ์ƒ‰์œผ๋กœ ์‚ฌ์šฉ๋˜๋Š” ์ƒ‰์ƒ์ด๋ฏ€๋กœ, ํ…์ŠคํŠธ ์ƒ‰์ƒ์œผ๋กœ ์‚ฌ์šฉํ•˜๋ฉด ๋ฐฐ๊ฒฝ๊ณผ ์ƒ‰์ด ๋น„์Šทํ•ด์ ธ ํ…์ŠคํŠธ๊ฐ€ ๋ณด์ด์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. Material3 ๋””์ž์ธ ๊ฐ€์ด๋“œ์— ๋”ฐ๋ผ DmsTheme.colorScheme.onSurface๋‚˜ DmsTheme.colorScheme.onSurfaceVariant์™€ ๊ฐ™์€ 'on' ๊ณ„์—ด ์ƒ‰์ƒ์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

Suggested change
titleContentColor: Color = DmsTheme.colorScheme.surfaceContainer,
textContentColor: Color = DmsTheme.colorScheme.surfaceContainer,
titleContentColor: Color = DmsTheme.colorScheme.onSurface,
textContentColor: Color = DmsTheme.colorScheme.onSurfaceVariant,

Comment on lines +102 to +104
Column(
modifier = Modifier.fillMaxSize(),
) {

Choose a reason for hiding this comment

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

high

ํ™”๋ฉด์˜ ๋ฃจํŠธ Column์— Modifier.navigationBarsPadding()์ด ์ ์šฉ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ์ œ์Šค์ฒ˜ ๋„ค๋น„๊ฒŒ์ด์…˜์„ ์‚ฌ์šฉํ•˜๋Š” ๊ธฐ๊ธฐ์—์„œ๋Š” ํ•˜๋‹จ UI ์š”์†Œ๋“ค์ด ์‹œ์Šคํ…œ ๋„ค๋น„๊ฒŒ์ด์…˜ ๋ฐ”์— ๊ฐ€๋ ค์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. DmsApp์—์„œ ์ „์—ญ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๋˜ navigationBarsPadding์ด ์ œ๊ฑฐ๋˜์—ˆ์œผ๋ฏ€๋กœ, ์ด์ œ ๊ฐ ํ™”๋ฉด์—์„œ ๊ฐœ๋ณ„์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ํ•˜๋‹จ์— UI ์š”์†Œ๊ฐ€ ์žˆ๋Š” ๋ชจ๋“  ์ƒˆ ํ™”๋ฉด์— ์ด ํŒจ๋”ฉ์„ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

Suggested change
Column(
modifier = Modifier.fillMaxSize(),
) {
Column(
modifier = Modifier.fillMaxSize().navigationBarsPadding(),
) {


private fun fetchNotificationStatus() {
viewModelScope.launch {
notificationRepository.fetchNotificationStatus(uiState.value.deviceToken.toString()).onSuccess { statuses ->

Choose a reason for hiding this comment

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

high

deviceToken์ด null์ผ ๊ฒฝ์šฐ uiState.value.deviceToken.toString() ํ˜ธ์ถœ ์‹œ NullPointerException์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. fetchDeviceToken()์ด ๋น„๋™๊ธฐ๋กœ ๋™์ž‘ํ•˜๋ฏ€๋กœ fetchNotificationStatus()๊ฐ€ ํ˜ธ์ถœ๋˜๋Š” ์‹œ์ ์— deviceToken์ด ์•„์ง ์„ค์ •๋˜์ง€ ์•Š์•˜์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. deviceToken์ด null์ด ์•„๋‹ ๋•Œ๋งŒ fetchNotificationStatus๋ฅผ ํ˜ธ์ถœํ•˜๊ฑฐ๋‚˜, deviceToken์„ ์•ˆ์ „ํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•ด์ฃผ์„ธ์š”.

            val deviceToken = uiState.value.deviceToken ?: return@launch
            notificationRepository.fetchNotificationStatus(deviceToken).onSuccess { statuses ->

Comment on lines 25 to 26
}.onFailure {
}

Choose a reason for hiding this comment

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

medium

studentRepository.fetchMyPage() ํ˜ธ์ถœ ์‹œ onFailure ๋ธ”๋ก์ด ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค. ๋„คํŠธ์›Œํฌ ์š”์ฒญ ์‹คํŒจ ๋“ฑ ์˜ˆ์™ธ ์ƒํ™ฉ์ด ๋ฐœ์ƒํ–ˆ์„ ๋•Œ ์‚ฌ์šฉ์ž์—๊ฒŒ ์•„๋ฌด๋Ÿฐ ํ”ผ๋“œ๋ฐฑ์ด ์ œ๊ณต๋˜์ง€ ์•Š์œผ๋ฉด ์•ฑ์ด ๋ฉˆ์ถ˜ ๊ฒƒ์ฒ˜๋Ÿผ ๋ณด์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์‹คํŒจ ์‹œ ์Šค๋‚ต๋ฐ”๋ฅผ ํ†ตํ•ด ์‚ฌ์šฉ์ž์—๊ฒŒ ์˜ค๋ฅ˜๋ฅผ ์•Œ๋ฆฌ๊ฑฐ๋‚˜, ์ตœ์†Œํ•œ ๋กœ๊ทธ๋ฅผ ๋‚จ๊ฒจ ๋””๋ฒ„๊น…์ด ์šฉ์ดํ•˜๋„๋ก ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

            }.onFailure { exception ->
                // TODO: post side effect to show error message
                exception.printStackTrace()
            }

Comment on lines +26 to +29
pointRepository.fetchPoints(
type = PointType.ALL,
page = null,
size = null,

Choose a reason for hiding this comment

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

medium

์ƒ๋ฒŒ์  ๋‚ด์—ญ์„ ์กฐํšŒํ•  ๋•Œ page์™€ size๋ฅผ null๋กœ ์ „๋‹ฌํ•˜์—ฌ ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ ํ•œ ๋ฒˆ์— ๊ฐ€์ ธ์˜ค๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ƒ๋ฒŒ์  ๋‚ด์—ญ์ด ๋งŽ์•„์งˆ ๊ฒฝ์šฐ ์•ฑ ์„ฑ๋Šฅ ์ €ํ•˜ ๋ฐ ๊ณผ๋„ํ•œ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ์„ ์œ ๋ฐœํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํŽ˜์ด์ง•(Paging)์„ ๊ตฌํ˜„ํ•˜์—ฌ ์‚ฌ์šฉ์ž๊ฐ€ ์Šคํฌ๋กคํ•  ๋•Œ๋งˆ๋‹ค ํ•„์š”ํ•œ ๋งŒํผ์˜ ๋ฐ์ดํ„ฐ๋งŒ ์ ์ง„์ ์œผ๋กœ ๋ถˆ๋Ÿฌ์˜ค๋„๋ก ๊ฐœ์„ ํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

Suggested change
pointRepository.fetchPoints(
type = PointType.ALL,
page = null,
size = null,
pointRepository.fetchPoints(
type = PointType.ALL,
page = 1, // TODO: Implement pagination
size = 20, // TODO: Implement pagination
).onSuccess { pointHistory ->

viewModel.sideEffect.collect {
when (it) {
is SelectProfileSideEffect.SuccessProfileImage -> onNavigateAdjustProfile(it.profileImageUrl)
is SelectProfileSideEffect.ProfileImageBadRequest -> onShowSnackBar(DmsSnackBarType.SUCCESS, "์—…๋กœ๋“œ ์„ฑ๊ณต!")

Choose a reason for hiding this comment

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

medium

ProfileImageBadRequest ์‚ฌ์ด๋“œ ์ดํŽ™ํŠธ๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๋•Œ "์—…๋กœ๋“œ ์„ฑ๊ณต!"์ด๋ผ๋Š” ์„ฑ๊ณต ์Šค๋‚ต๋ฐ”๋ฅผ ๋ณด์—ฌ์ฃผ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” ์‚ฌ์šฉ์ž์—๊ฒŒ ํ˜ผ๋ž€์„ ์ค„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. BadRequest๋Š” ๋ณดํ†ต ์˜ค๋ฅ˜ ์ƒํ™ฉ์„ ์˜๋ฏธํ•˜๋ฏ€๋กœ, ์˜ค๋ฅ˜ ์Šค๋‚ต๋ฐ”๋ฅผ ํ‘œ์‹œํ•˜๊ณ  ๋ฉ”์‹œ์ง€๋„ "์ž˜๋ชป๋œ ์š”์ฒญ์ž…๋‹ˆ๋‹ค." ์™€ ๊ฐ™์ด ์˜ค๋ฅ˜ ์ƒํ™ฉ์— ๋งž๊ฒŒ ์ˆ˜์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

Suggested change
is SelectProfileSideEffect.ProfileImageBadRequest -> onShowSnackBar(DmsSnackBarType.SUCCESS, "์—…๋กœ๋“œ ์„ฑ๊ณต!")
is SelectProfileSideEffect.ProfileImageBadRequest -> onShowSnackBar(DmsSnackBarType.ERROR, "์ด๋ฏธ์ง€๋ฅผ ์„ ํƒํ•ด์ฃผ์„ธ์š”.")

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: 7

๐Ÿค– Fix all issues with AI agents
In @app/src/dev/kotlin/team/aliens/dms/android/app/DmsApp.kt:
- Around line 75-76: The ResetPasswordScreenNav data class currently carries
currentPassword which exposes secrets in the navigation back stack; remove the
currentPassword field from ResetPasswordScreenNav and stop passing the password
via NavKey, instead keep the password in a transient, non-navigation holder such
as a shared ViewModel (e.g., a ResetPasswordViewModel) or use the ResultStore to
transfer the value, or require re-validation on the ResetPassword screen; update
any callers that construct ResetPasswordScreenNav and consumers that read it to
use the chosen alternative (shared ViewModel or ResultStore) and ensure no
logging/serialization of the password occurs.

In
@feature/src/dev/kotlin/team/aliens/dms/android/feature/profile/viewmodel/SelectProfileViewModel.kt:
- Around line 84-106: fetchPresignedUrl is called outside the runCatching, so
exceptions from fileRepository.fetchPresignedUrl(file.name) will escape and not
trigger the FailProfileImage side effect; wrap both the fetchPresignedUrl and
the subsequent uploadFile call inside a single runCatching (or a try/catch)
within viewModelScope.launch(Dispatchers.IO) so any exception during fetch or
upload is caught, and onFailure calls
sendEffect(SelectProfileSideEffect.FailProfileImage) (and update state as
needed, e.g., disable button) while onSuccess processes fileUrl as before.

In
@feature/src/dev/kotlin/team/aliens/dms/android/feature/resetpassword/viewmodel/ResetPasswordViewModel.kt:
- Line 14: The userRepository property in ResetPasswordViewModel is publicly
exposed; make it private by changing its declaration to a private val so the
injected UserRepository cannot be accessed externally and the class follows
encapsulation best practices (update the ResetPasswordViewModel's userRepository
declaration to private val userRepository: UserRepository).
- Line 3: The file ResetPasswordViewModel.kt currently imports android.util.Log
but never uses it; remove the unused import statement for android.util.Log from
ResetPasswordViewModel.kt to clean up imports and eliminate the unused import
warning.
- Around line 34-56: In resetPassword in ResetPasswordViewModel: validate the
actual newPassword (use uiState.value.newPassword) when calling
checkIfPasswordValid and pass newPassword to userRepository.editPassword rather
than uiState.value.checkNewPassword to make intent clear and ensure youโ€™re
updating to the primary input; also fix the failure message construction in the
onFailure handler to avoid the literal "null" by using a safe fallback (e.g.,
prefer it.cause?.message, then it.message, then a default string) when calling
sendEffect(ResetPasswordSideEffect.FailResetPassword).

In
@feature/src/dev/kotlin/team/aliens/dms/android/feature/setting/ui/SettingScreen.kt:
- Around line 103-105: The root Column in SettingScreen (the Composable where
Column is created with modifier = Modifier.fillMaxSize()) can have its bottom
content obscured by system gesture/navigation bars; update the Column modifier
to include navigationBarsPadding by combining Modifier.fillMaxSize() with
Modifier.navigationBarsPadding() (e.g.,
Modifier.fillMaxSize().navigationBarsPadding()) so the UI respects system
navigation insets.

In
@feature/src/dev/kotlin/team/aliens/dms/android/feature/setting/viewmodel/SettingViewModel.kt:
- Around line 65-70: The updateNotificationStatus function currently flips UI
state optimistically using the incoming isOnNotification parameter, checks the
old value, and only calls a placeholder setNotificationStatus() for unsubscribe;
fix by making the UI update reflect the result of a backend operation (or
disable the toggle until backend exists): in updateNotificationStatus, determine
the intended new state (newValue = !isOnNotification), call an implemented async
operation that handles both subscribe and unsubscribe (implement
subscribe/unsubscribe logic inside setNotificationStatus or split into
subscribeToNotifications / unsubscribeFromNotifications), await the backend
response, only then update state via setState(settingState ->
settingState.copy(isOnNotification = newValue)) and on failure revert state and
surface an error; if backend integration is unavailable, disable the toggle path
by preventing state change and returning an error message instead of
optimistically toggling.
๐Ÿงน Nitpick comments (12)
feature/src/dev/kotlin/team/aliens/dms/android/feature/main/mypage/viewmodel/MyPageViewModel.kt (1)

21-29: Consider logging the exception for debugging purposes.

The error handling now properly sends a side effect to notify the user, which is good. However, logging the exception would help with debugging production issues.

๐Ÿ”Ž Proposed improvement
         }.onFailure {
+            it.printStackTrace() // or use a logging framework
             sendEffect(MyPageSideEffect.FailFetchMyPage("๋งˆ์ดํŽ˜์ด์ง€ ์ •๋ณด ์กฐํšŒ๋ฅผ ์‹คํŒจํ–ˆ์–ด์š”"))
         }
feature/src/dev/kotlin/team/aliens/dms/android/feature/resetpassword/ui/CheckPasswordScreen.kt (2)

43-52: Potential stale state capture in side effect handler.

At Line 46, state.currentPassword is captured when the side effect is emitted, but state is obtained outside the collect block. If the password changes between emission and navigation, the stale value could be passed. Consider passing the password directly from the side effect:

is CheckPasswordSideEffect.SuccessCheckPassword -> onNavigateResetPassword(it.password)

This requires updating SuccessCheckPassword to carry the validated password.


89-99: Consider setting keyboardType = KeyboardType.Password for the password field.

The password text field doesn't specify a keyboardType. Setting keyboardType = KeyboardType.Password provides a better UX by showing a keyboard optimized for password entry.

๐Ÿ”Ž Suggested change
 DmsTextField(
     modifier = Modifier
         .fillMaxWidth()
         .padding(top = 32.dp)
         .horizontalPadding(24.dp),
     label = "๋น„๋ฐ€๋ฒˆํ˜ธ",
     value = state.currentPassword,
     hint = "๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ",
     onValueChange = onPasswordChange,
     showVisibleIcon = true,
+    keyboardType = KeyboardType.Password,
 )
feature/src/dev/kotlin/team/aliens/dms/android/feature/resetpassword/ui/ResetPasswordScreen.kt (1)

94-115: Add keyboardType = KeyboardType.Password to both password fields.

Both password text fields should specify the password keyboard type for better UX.

๐Ÿ”Ž Suggested change
 DmsTextField(
     modifier = Modifier
         .fillMaxWidth()
         .padding(top = 32.dp)
         .horizontalPadding(24.dp),
     label = "๋น„๋ฐ€๋ฒˆํ˜ธ",
     value = state.newPassword,
     hint = "๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ",
     onValueChange = onNewPasswordChange,
     showVisibleIcon = true,
+    keyboardType = KeyboardType.Password,
 )
 DmsTextField(
     modifier = Modifier
         .fillMaxWidth()
         .padding(top = 32.dp)
         .horizontalPadding(24.dp),
     label = "๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์ž…๋ ฅ",
     value = state.checkNewPassword,
     hint = "๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์ž…๋ ฅ",
     onValueChange = onCheckNewPasswordChange,
     showVisibleIcon = true,
+    keyboardType = KeyboardType.Password,
 )
feature/src/dev/kotlin/team/aliens/dms/android/feature/resetpassword/viewmodel/ResetPasswordViewModel.kt (2)

27-32: Rename misleading variable isSignInValueNotBlank.

The variable name references "SignIn" but this is password reset logic. Consider a more descriptive name like isPasswordFieldsValid.

๐Ÿ”Ž Suggested fix
 private fun setButtonEnabled() = setState {
     with(uiState.value) {
-        val isSignInValueNotBlank = newPassword.isNotBlank() && checkNewPassword.isNotBlank() && newPassword == checkNewPassword
-        it.copy(buttonEnabled = isSignInValueNotBlank)
+        val isPasswordFieldsValid = newPassword.isNotBlank() && checkNewPassword.isNotBlank() && newPassword == checkNewPassword
+        it.copy(buttonEnabled = isPasswordFieldsValid)
     }
 }

66-70: Consider consolidating FailResetPassword and PasswordMismatch side effects.

Both FailResetPassword and PasswordMismatch are handled identically in the UI (showing an error snackbar). Consider whether a single error side effect with a message would simplify the code, or if the semantic distinction is intentional for future differentiation.

feature/src/dev/kotlin/team/aliens/dms/android/feature/profile/viewmodel/SelectProfileViewModel.kt (2)

28-52: Consider batching state updates for better performance.

The current implementation updates the state on every iteration (line 49), which could trigger many re-compositions if the device contains hundreds or thousands of images. If the incremental loading is not required for the UI, consider collecting all URIs first and updating the state once after the loop completes.

๐Ÿ”Ž Potential optimization
     cursor?.use {
         val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
         while (cursor.moveToNext()) {
             val id = cursor.getLong(idColumn)
             val imageUri = ContentUris.withAppendedId(contentUri, id)
             uriList.add(imageUri.toString())
-            setState { it.copy(uriList = uriList) }
         }
+        setState { it.copy(uriList = uriList) }
     }

However, if the current implementation is intentional to provide progressive loading feedback to the UI, you can safely ignore this suggestion.


84-84: Rename function to reflect its actual behavior.

The function is named fetchPresignedUrl but it actually performs both fetching the presigned URL and uploading the file. Consider renaming it to better reflect its complete behavior, such as uploadFileWithPresignedUrl or uploadFile.

feature/src/dev/kotlin/team/aliens/dms/android/feature/setting/ui/SettingScreen.kt (1)

29-30: Remove unused imports.

The imports titleB and titleM are not used anywhere in this file.

๐Ÿ”Ž Proposed fix
-import team.aliens.dms.android.core.designsystem.titleB
-import team.aliens.dms.android.core.designsystem.titleM
feature/src/dev/kotlin/team/aliens/dms/android/feature/setting/viewmodel/SettingViewModel.kt (3)

15-18: Make injected dependencies private.

The constructor parameters are declared as val, which exposes them as public properties. ViewModels should encapsulate their dependencies as implementation details.

๐Ÿ”Ž Proposed fix
 @HiltViewModel
 class SettingViewModel @Inject constructor(
-    val notificationRepository: NotificationRepository,
-    val authRepository: AuthRepository,
-    val deviceDataStoreDataSource: DeviceDataStoreDataSource,
+    private val notificationRepository: NotificationRepository,
+    private val authRepository: AuthRepository,
+    private val deviceDataStoreDataSource: DeviceDataStoreDataSource,
 ): BaseStateViewModel<SettingState, SettingSideEffect>(SettingState()) {

40-46: Add error handling for sign-out failures.

The signOut function only handles the success case. If the sign-out fails (e.g., network error), the user receives no feedback and remains in an ambiguous state.

๐Ÿ”Ž Proposed fix
     internal fun signOut() {
         viewModelScope.launch(Dispatchers.IO) {
             authRepository.signOut().onSuccess {
                 sendEffect(SettingSideEffect.SignOutSuccess)
+            }.onFailure {
+                // Consider adding a side effect like SettingSideEffect.SignOutFailed
+                // to show an error message to the user
             }
         }
     }

78-78: Reconsider default value for isOnNotification.

The default value is true, but the actual notification status is unknown until fetchNotificationStatus() completes. This could mislead users into thinking notifications are enabled when they might not be. Consider defaulting to false or introducing a separate loading state.

 data class SettingState(
     val deviceToken: String? = null,
-    val isOnNotification: Boolean = true,
+    val isOnNotification: Boolean = false,
     val notificationTopicStatus: List<NotificationTopicGroup.Status> = emptyList()
 )
๐Ÿ“œ Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

๐Ÿ“ฅ Commits

Reviewing files that changed from the base of the PR and between e1fd3a8 and 3d147f0.

๐Ÿ“’ Files selected for processing (15)
  • app/src/dev/AndroidManifest.xml
  • app/src/dev/kotlin/team/aliens/dms/android/app/DmsApp.kt
  • data/src/dev/kotlin/team.aliens.dms.android.data/user/repository/UserRepositoryImpl.kt
  • feature/src/dev/kotlin/team/aliens/dms/android/feature/main/mypage/navigation/MyPageRoute.kt
  • feature/src/dev/kotlin/team/aliens/dms/android/feature/main/mypage/ui/MyPageScreen.kt
  • feature/src/dev/kotlin/team/aliens/dms/android/feature/main/mypage/viewmodel/MyPageViewModel.kt
  • feature/src/dev/kotlin/team/aliens/dms/android/feature/profile/ui/SelectProfileScreen.kt
  • feature/src/dev/kotlin/team/aliens/dms/android/feature/profile/viewmodel/SelectProfileViewModel.kt
  • feature/src/dev/kotlin/team/aliens/dms/android/feature/resetpassword/navigation/CheckPasswordRoute.kt
  • feature/src/dev/kotlin/team/aliens/dms/android/feature/resetpassword/navigation/ResetPasswordRoute.kt
  • feature/src/dev/kotlin/team/aliens/dms/android/feature/resetpassword/ui/CheckPasswordScreen.kt
  • feature/src/dev/kotlin/team/aliens/dms/android/feature/resetpassword/ui/ResetPasswordScreen.kt
  • feature/src/dev/kotlin/team/aliens/dms/android/feature/resetpassword/viewmodel/ResetPasswordViewModel.kt
  • feature/src/dev/kotlin/team/aliens/dms/android/feature/setting/ui/SettingScreen.kt
  • feature/src/dev/kotlin/team/aliens/dms/android/feature/setting/viewmodel/SettingViewModel.kt
๐Ÿšง Files skipped from review as they are similar to previous changes (4)
  • data/src/dev/kotlin/team.aliens.dms.android.data/user/repository/UserRepositoryImpl.kt
  • feature/src/dev/kotlin/team/aliens/dms/android/feature/profile/ui/SelectProfileScreen.kt
  • feature/src/dev/kotlin/team/aliens/dms/android/feature/resetpassword/navigation/ResetPasswordRoute.kt
  • feature/src/dev/kotlin/team/aliens/dms/android/feature/resetpassword/navigation/CheckPasswordRoute.kt
๐Ÿงฐ Additional context used
๐Ÿงฌ Code graph analysis (10)
feature/src/dev/kotlin/team/aliens/dms/android/feature/main/mypage/viewmodel/MyPageViewModel.kt (2)
core/ui/src/dev/java/team/aliens/dms/android/core/ui/viewmodel/BaseStateViewModel.kt (2)
  • setState (29-31)
  • sendEffect (33-35)
feature/src/dev/kotlin/team/aliens/dms/android/feature/main/mypage/ui/MyPageScreen.kt (1)
  • MyPage (30-54)
feature/src/dev/kotlin/team/aliens/dms/android/feature/setting/viewmodel/SettingViewModel.kt (1)
core/ui/src/dev/java/team/aliens/dms/android/core/ui/viewmodel/BaseStateViewModel.kt (2)
  • setState (29-31)
  • sendEffect (33-35)
app/src/dev/kotlin/team/aliens/dms/android/app/DmsApp.kt (14)
feature/src/dev/kotlin/team/aliens/dms/android/feature/onboarding/navigation/OnboardingRoute.kt (1)
  • OnboardingRoute (6-13)
feature/src/dev/kotlin/team/aliens/dms/android/feature/signin/navigation/SignInRoute.kt (1)
  • SignInRoute (7-20)
feature/src/dev/kotlin/team/aliens/dms/android/feature/main/home/navigation/HomeRoute.kt (1)
  • HomeRoute (9-22)
feature/src/dev/kotlin/team/aliens/dms/android/feature/main/application/navigation/ApplicationRoute.kt (1)
  • ApplicationRoute (9-24)
feature/src/dev/kotlin/team/aliens/dms/android/feature/vote/navigation/VoteRoute.kt (1)
  • VoteRoute (8-23)
feature/src/dev/kotlin/team/aliens/dms/android/feature/remain/navigation/RemainApplicationRoute.kt (1)
  • RemainApplicationRoute (7-16)
feature/src/dev/kotlin/team/aliens/dms/android/feature/main/mypage/navigation/MyPageRoute.kt (1)
  • MyPageRoute (8-19)
feature/src/dev/kotlin/team/aliens/dms/android/feature/meal/navigation/MealRoute.kt (1)
  • MealRoute (6-13)
feature/src/dev/kotlin/team/aliens/dms/android/feature/setting/navigation/SettingRoute.kt (1)
  • SettingRoute (7-22)
feature/src/dev/kotlin/team/aliens/dms/android/feature/point/navigation/PointHistoryRoute.kt (1)
  • PointHistoryRoute (7-14)
feature/src/dev/kotlin/team/aliens/dms/android/feature/resetpassword/navigation/ResetPasswordRoute.kt (1)
  • ResetPasswordRoute (8-21)
feature/src/dev/kotlin/team/aliens/dms/android/feature/resetpassword/navigation/CheckPasswordRoute.kt (1)
  • CheckPasswordRoute (7-18)
feature/src/dev/kotlin/team/aliens/dms/android/feature/profile/route/SelectProfileRoute.kt (1)
  • SelectProfileRoute (7-18)
feature/src/dev/kotlin/team/aliens/dms/android/feature/profile/route/AdjustProfileRoute.kt (1)
  • AdjustProfileRoute (8-19)
feature/src/dev/kotlin/team/aliens/dms/android/feature/resetpassword/ui/CheckPasswordScreen.kt (3)
core/design-system/src/dev/java/team/aliens/dms/android/core/designsystem/foundation/DmsSymbol.kt (1)
  • DmsSymbolContent (18-52)
core/design-system/src/dev/java/team/aliens/dms/android/core/designsystem/textfield/DmsTextField.kt (1)
  • DmsTextField (39-107)
core/design-system/src/dev/java/team/aliens/dms/android/core/designsystem/button/Button.kt (1)
  • DmsButton (260-356)
feature/src/dev/kotlin/team/aliens/dms/android/feature/profile/viewmodel/SelectProfileViewModel.kt (1)
core/ui/src/dev/java/team/aliens/dms/android/core/ui/viewmodel/BaseStateViewModel.kt (2)
  • setState (29-31)
  • sendEffect (33-35)
feature/src/dev/kotlin/team/aliens/dms/android/feature/resetpassword/ui/ResetPasswordScreen.kt (3)
core/design-system/src/dev/java/team/aliens/dms/android/core/designsystem/foundation/DmsSymbol.kt (1)
  • DmsSymbolContent (18-52)
core/design-system/src/dev/java/team/aliens/dms/android/core/designsystem/textfield/DmsTextField.kt (1)
  • DmsTextField (39-107)
core/design-system/src/dev/java/team/aliens/dms/android/core/designsystem/button/Button.kt (1)
  • DmsButton (260-356)
feature/src/dev/kotlin/team/aliens/dms/android/feature/main/mypage/navigation/MyPageRoute.kt (1)
feature/src/dev/kotlin/team/aliens/dms/android/feature/main/mypage/ui/MyPageScreen.kt (1)
  • MyPage (30-54)
feature/src/dev/kotlin/team/aliens/dms/android/feature/main/mypage/ui/MyPageScreen.kt (5)
core/design-system/src/dev/java/team/aliens/dms/android/core/designsystem/button/DmsIconButton.kt (1)
  • DmsIconButton (17-40)
feature/src/dev/kotlin/team/aliens/dms/android/feature/main/mypage/ui/component/ProfileContent.kt (1)
  • ProfileContent (31-77)
feature/src/dev/kotlin/team/aliens/dms/android/feature/main/mypage/ui/component/PhraseContent.kt (1)
  • PhraseContent (16-34)
feature/src/dev/kotlin/team/aliens/dms/android/feature/main/home/model/DmsPointCotent.kt (1)
  • DmsPointContent (25-71)
core/design-system/src/dev/java/team/aliens/dms/android/core/designsystem/button/DmsItemButton.kt (1)
  • DmsItemButton (33-84)
feature/src/dev/kotlin/team/aliens/dms/android/feature/resetpassword/viewmodel/ResetPasswordViewModel.kt (2)
core/ui/src/dev/java/team/aliens/dms/android/core/ui/viewmodel/BaseStateViewModel.kt (2)
  • setState (29-31)
  • sendEffect (33-35)
shared/validator/src/main/java/team/aliens/dms/android/shared/validator/PasswordValidator.kt (1)
  • checkIfPasswordValid (11-11)
feature/src/dev/kotlin/team/aliens/dms/android/feature/setting/ui/SettingScreen.kt (3)
core/design-system/src/dev/java/team/aliens/dms/android/core/designsystem/button/Button.kt (1)
  • DmsButton (260-356)
core/design-system/src/dev/java/team/aliens/dms/android/core/designsystem/button/DmsItemButton.kt (1)
  • DmsItemButton (33-84)
feature/src/dev/kotlin/team/aliens/dms/android/feature/setting/ui/component/SettingRotateContent.kt (1)
  • SettingRotateContent (30-74)
โฐ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
๐Ÿ”‡ Additional comments (14)
feature/src/dev/kotlin/team/aliens/dms/android/feature/main/mypage/navigation/MyPageRoute.kt (1)

9-19: LGTM!

Clean routing implementation that properly forwards navigation and snackbar callbacks to the MyPage composable. The parameter types align correctly with the MyPage signature.

feature/src/dev/kotlin/team/aliens/dms/android/feature/main/mypage/viewmodel/MyPageViewModel.kt (1)

32-38: LGTM!

Clean state and side effect definitions. The sealed interface pattern for MyPageSideEffect ensures exhaustive handling in the UI layer.

feature/src/dev/kotlin/team/aliens/dms/android/feature/main/mypage/ui/MyPageScreen.kt (2)

30-54: LGTM!

Well-structured composable with proper separation of concerns:

  • ViewModel injection via hiltViewModel()
  • State collection with lifecycle awareness using collectAsStateWithLifecycle()
  • Side effect handling via LaunchedEffect that correctly collects from the ViewModel's side effect flow

The exhaustive when block ensures all side effects are handled.


56-107: LGTM!

Clean stateless UI implementation that properly:

  • Separates the screen rendering from state management
  • Uses design system components consistently (DmsTopAppBar, DmsIconButton, DmsItemButton, DmsPointContent)
  • Composes profile, phrase, and point content from the state

Minor note: modifier = Modifier on line 96 is the default value and could be omitted, but it's harmless.

app/src/dev/AndroidManifest.xml (1)

10-11: Manifest permissions correctly configured for SDK version compatibility.

The permission declarations properly address the API level differences:

  • READ_MEDIA_IMAGES for Android 13+ (API 33+)
  • READ_EXTERNAL_STORAGE with maxSdkVersion="32" for Android 12L and below

The runtime permission logic in SelectProfileScreen.kt (lines 58-62) correctly branches based on Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU and requests the appropriate permission for each SDK level, matching the manifest configuration.

feature/src/dev/kotlin/team/aliens/dms/android/feature/resetpassword/ui/ResetPasswordScreen.kt (1)

33-66: LGTM on the side effect handling and ViewModel wiring.

The composable correctly wires the ViewModel, collects side effects lifecycle-aware, and passes currentPassword to the reset function only when needed. The separation between the wiring composable and the stateless screen composable follows good practices.

feature/src/dev/kotlin/team/aliens/dms/android/feature/profile/viewmodel/SelectProfileViewModel.kt (4)

17-21: LGTM!

The ViewModel setup with Hilt dependency injection is correct. The injected dependencies (Context and FileRepository) are appropriate for the file selection and upload operations.


54-66: LGTM!

The image selection toggle logic is clear and correct. It properly handles both selecting and deselecting images.


68-82: LGTM!

The null check and delegation pattern is appropriate. The function correctly handles the invalid case by emitting a ProfileImageBadRequest side effect.


109-123: LGTM!

The state and side effect definitions are well-structured. The use of data object for singleton side effects is the correct Kotlin pattern.

app/src/dev/kotlin/team/aliens/dms/android/app/DmsApp.kt (4)

26-40: LGTM - Imports properly aligned with new features.

The new imports for PointType and the additional route components support the navigation features being added in this PR.


87-87: Verify runtime JWT status changes are handled correctly.

The parameter type changed from StateFlow<Boolean> to plain Boolean, which means isJwtAvailable is no longer reactive. If the JWT token becomes available or is invalidated during runtime (e.g., after login/logout), this won't trigger a recomposition or navigation update automatically.

Confirm this is intentional and that JWT changes are handled through explicit navigation actions (e.g., logout navigates to SignIn explicitly) rather than reactive updates.


142-289: LGTM - Navigation implementation is comprehensive and well-structured.

The navigation setup properly implements all the new screens with correct back stack management:

  • Object-based NavKeys use direct reference removal
  • Data class NavKeys create instances with matching parameters for removal (proper equals/hashCode from @serializable)
  • Multi-step flows like password reset (lines 254-255) correctly pop multiple screens
  • All routes consistently wire up callbacks and snackbar handling

The entryProvider pattern is properly utilized for Compose Navigation 3.


138-138: LGTM - Correct padding usage with Scaffold.

Using padding(paddingValues) from Scaffold is the appropriate approach, as it automatically handles system bar insets including navigation bars.

Comment on lines +75 to +76
@Serializable
data class ResetPasswordScreenNav(val currentPassword: String) : NavKey
Copy link

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue | ๐ŸŸ  Major

Security: Consider not passing currentPassword via navigation parameter.

Passing the current password as a navigation parameter means it will be stored in the back stack and could be logged or exposed through debugging tools. Consider alternative approaches such as:

  • Using a shared ViewModel to hold the password temporarily
  • Storing it in the ResultStore
  • Re-validating the password in the ResetPassword screen
๐Ÿค– Prompt for AI Agents
In @app/src/dev/kotlin/team/aliens/dms/android/app/DmsApp.kt around lines 75 -
76, The ResetPasswordScreenNav data class currently carries currentPassword
which exposes secrets in the navigation back stack; remove the currentPassword
field from ResetPasswordScreenNav and stop passing the password via NavKey,
instead keep the password in a transient, non-navigation holder such as a shared
ViewModel (e.g., a ResetPasswordViewModel) or use the ResultStore to transfer
the value, or require re-validation on the ResetPassword screen; update any
callers that construct ResetPasswordScreenNav and consumers that read it to use
the chosen alternative (shared ViewModel or ResultStore) and ensure no
logging/serialization of the password occurs.

Comment on lines +103 to +105
Column(
modifier = Modifier.fillMaxSize(),
) {
Copy link

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue | ๐ŸŸก Minor

Add navigationBarsPadding to prevent UI overlap with system navigation.

As noted in previous reviews, the root Column needs Modifier.navigationBarsPadding() to avoid bottom UI elements being obscured by gesture navigation bars on devices that use them.

๐Ÿ”Ž Proposed fix
     Column(
-        modifier = Modifier.fillMaxSize(),
+        modifier = Modifier.fillMaxSize().navigationBarsPadding(),
     ) {
๐Ÿ“ 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
Column(
modifier = Modifier.fillMaxSize(),
) {
Column(
modifier = Modifier.fillMaxSize().navigationBarsPadding(),
) {
๐Ÿค– Prompt for AI Agents
In
@feature/src/dev/kotlin/team/aliens/dms/android/feature/setting/ui/SettingScreen.kt
around lines 103 - 105, The root Column in SettingScreen (the Composable where
Column is created with modifier = Modifier.fillMaxSize()) can have its bottom
content obscured by system gesture/navigation bars; update the Column modifier
to include navigationBarsPadding by combining Modifier.fillMaxSize() with
Modifier.navigationBarsPadding() (e.g.,
Modifier.fillMaxSize().navigationBarsPadding()) so the UI respects system
navigation insets.

Comment on lines +65 to +70
internal fun updateNotificationStatus(isOnNotification: Boolean) {
setState { settingState ->
settingState.copy(isOnNotification = !isOnNotification)
}
if (isOnNotification) /* ๊ตฌ๋… ์ทจ์†Œ */ setNotificationStatus() else return // TODO ๊ตฌ๋… ์—…๋ฐ์ดํŠธ (false -> true)
}
Copy link

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue | ๐Ÿ”ด Critical

Critical: Incomplete notification toggle implementation causes state inconsistency.

The function has several issues:

  1. Line 67 optimistically toggles isOnNotification in the UI state before any backend operation
  2. Line 69 only implements unsubscribe (when isOnNotification was true) but not subscribe (TODO comment confirms this)
  3. The condition checks the OLD parameter value, not the new toggled value, which is confusing
  4. setNotificationStatus() (lines 72-73) is an empty placeholder

This creates a critical state inconsistency: the UI shows the toggle switched, but the backend subscription state might not change (especially for the falseโ†’true case). Users will see notifications as "ON" but won't receive any.

๐Ÿ”Ž Recommended approach

Option 1: Disable the toggle until the feature is fully implemented:

     internal fun updateNotificationStatus(isOnNotification: Boolean) {
-        setState { settingState ->
-            settingState.copy(isOnNotification = !isOnNotification)
-        }
-        if (isOnNotification) /* ๊ตฌ๋… ์ทจ์†Œ */ setNotificationStatus() else return // TODO ๊ตฌ๋… ์—…๋ฐ์ดํŠธ (false -> true)
+        // TODO: Implement complete notification toggle
+        return
     }

Option 2: Implement the complete flow (requires backend implementation):

     internal fun updateNotificationStatus(isOnNotification: Boolean) {
-        setState { settingState ->
-            settingState.copy(isOnNotification = !isOnNotification)
-        }
-        if (isOnNotification) /* ๊ตฌ๋… ์ทจ์†Œ */ setNotificationStatus() else return // TODO ๊ตฌ๋… ์—…๋ฐ์ดํŠธ (false -> true)
+        val newValue = !isOnNotification
+        viewModelScope.launch {
+            val deviceToken = uiState.value.deviceToken ?: return@launch
+            if (newValue) {
+                // Subscribe to notifications
+                notificationRepository.subscribeToTopics(deviceToken, /*topics*/)
+            } else {
+                // Unsubscribe from notifications
+                notificationRepository.unsubscribeFromTopics(deviceToken, /*topics*/)
+            }
+            // Only update UI state after successful backend call
+            setState { it.copy(isOnNotification = newValue) }
+        }
     }

Would you like me to help implement the complete notification toggle flow, or should this feature be disabled until the backend integration is ready?

๐Ÿค– Prompt for AI Agents
In
@feature/src/dev/kotlin/team/aliens/dms/android/feature/setting/viewmodel/SettingViewModel.kt
around lines 65 - 70, The updateNotificationStatus function currently flips UI
state optimistically using the incoming isOnNotification parameter, checks the
old value, and only calls a placeholder setNotificationStatus() for unsubscribe;
fix by making the UI update reflect the result of a backend operation (or
disable the toggle until backend exists): in updateNotificationStatus, determine
the intended new state (newValue = !isOnNotification), call an implemented async
operation that handles both subscribe and unsubscribe (implement
subscribe/unsubscribe logic inside setNotificationStatus or split into
subscribeToNotifications / unsubscribeFromNotifications), await the backend
response, only then update state via setState(settingState ->
settingState.copy(isOnNotification = newValue)) and on failure revert state and
surface an error; if backend integration is unavailable, disable the toggle path
by preventing state change and returning an error message instead of
optimistically toggling.

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: 4

๐Ÿค– Fix all issues with AI agents
In
@feature/src/dev/kotlin/team/aliens/dms/android/feature/profile/viewmodel/SelectProfileViewModel.kt:
- Around line 82-104: The call to fileRepository.fetchPresignedUrl(file.name) in
SelectProfileViewModel.fetchPresignedUrl is executed outside the runCatching, so
any exception it throws will escape the coroutine; wrap the fetchPresignedUrl
call inside the same runCatching (or surround it with try/catch) so both
fetching the presigned info and fileRepository.uploadFile are guarded, and on
any failure call sendEffect(SelectProfileSideEffect.FailProfileImage) and
preserve the existing onSuccess handling that updates state and emits
SuccessProfileImage.
- Around line 28-50: The MediaStore query in getUriImage blocks the calling
thread; wrap the contentResolver.query and cursor iteration inside a background
IO coroutine using withContext(Dispatchers.IO) so the heavy I/O runs off the UI
thread, then post the resulting uriList back to the main thread (or call
setState from the coroutine after switching back) to update state; specifically
modify getUriImage (and any callers like loadImagesIfNeeded) to perform the
query + cursor.use inside Dispatchers.IO and only update setState on the main
dispatcher.
- Around line 66-80: uploadProfileImage currently calls
team.aliens.dms.android.core.file.File.toFile(...) on the calling thread which
may perform blocking I/O; move the URI-to-File conversion onto the IO dispatcher
by launching a coroutine on Dispatchers.IO (or using
viewModelScope.launch(Dispatchers.IO)) and call File.toFile inside that
coroutine, then switch back to Main if needed to call setState/sendEffect or
call fetchPresignedUrl from the IO coroutine; also ensure fetchPresignedUrl's
contract is clear (either remove its internal coroutine launch so it can be
called from this coroutine or keep it and only pass a ready File) to avoid
double-launching.
๐Ÿงน Nitpick comments (9)
app/src/dev/kotlin/team/aliens/dms/android/app/DmsApp.kt (1)

261-268: Password passed to navigation at line 264.

This is where the password gets passed to ResetPasswordScreenNav. As noted above, consider using resultStore (similar to how vote_result is handled on line 184) to pass the password transiently instead:

โ™ป๏ธ Alternative approach using ResultStore
 entry<CheckPasswordScreenNav> {
     CheckPasswordRoute(
         onBackPressed = { backStack.remove(CheckPasswordScreenNav) },
-        onNavigateResetPassword = { backStack.add(ResetPasswordScreenNav(it)) },
+        onNavigateResetPassword = { password ->
+            resultStore.setResult<String?>("current_password", password)
+            backStack.add(ResetPasswordScreenNav)
+        },
         onShowSnackBar = { snackBar, message ->
             appState.showSnackBar(snackBar, message)
         }
     )
 }

This would require changing ResetPasswordScreenNav to a data object and retrieving the password from resultStore in the ResetPasswordRoute.

feature/src/dev/kotlin/team/aliens/dms/android/feature/resetpassword/viewmodel/ResetPasswordViewModel.kt (4)

26-26: Track or address the TODO comment.

The TODO suggests future work on text field description message handling. Consider creating a tracking issue or implementing the logic now if it's essential for this feature.

Would you like me to open a tracking issue for this TODO, or would you prefer to implement the text field description message handling in this PR?


28-28: Rename variable for context clarity.

The variable name isSignInValueNotBlank appears to be a copy-paste artifact from a sign-in screen. In the password reset context, consider renaming it to something like arePasswordFieldsValid or isPasswordInputValid.

โ™ป๏ธ Proposed fix
-            val isSignInValueNotBlank = newPassword.isNotBlank() && checkNewPassword.isNotBlank() && newPassword == checkNewPassword
-            it.copy(buttonEnabled = isSignInValueNotBlank)
+            val arePasswordFieldsValid = newPassword.isNotBlank() && checkNewPassword.isNotBlank() && newPassword == checkNewPassword
+            it.copy(buttonEnabled = arePasswordFieldsValid)

33-56: Use newPassword as the primary password field for semantic clarity.

Throughout the resetPassword function, checkNewPassword is used as the "source of truth" for the new password (lines 34, 38, 46). Semantically, newPassword should be the primary input and checkNewPassword should be the confirmation field. While the current implementation is functionally correct (since setButtonEnabled enforces equality), using checkNewPassword as the actual password to set is semantically backwards and reduces code clarity.

Consider updating to use newPassword consistently as the primary field.

โ™ป๏ธ Proposed fix
 internal fun resetPassword(currentPassword: String) = run {
-    if(!checkIfPasswordValid(uiState.value.checkNewPassword)) {
+    if(!checkIfPasswordValid(uiState.value.newPassword)) {
         sendEffect(ResetPasswordSideEffect.PasswordMismatch("๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ํ˜•์‹์— ๋งž์ง€ ์•Š์Šต๋‹ˆ๋‹ค"))
         return@run
     }
-    if (currentPassword == uiState.value.checkNewPassword) {
+    if (currentPassword == uiState.value.newPassword) {
         sendEffect(ResetPasswordSideEffect.PasswordMismatch("๊ธฐ์กด ๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ๋ณ€๊ฒฝ ๋ถˆ๊ฐ€ํ•ฉ๋‹ˆ๋‹ค"))
         return@run
     }
     viewModelScope.launch {
         setState { it.copy(isLoading = true, buttonEnabled = false) }
         userRepository.editPassword(
             currentPassword = currentPassword,
-            newPassword = uiState.value.checkNewPassword,
+            newPassword = uiState.value.newPassword,
         ).onSuccess {
             setState { it.copy(isLoading = false, buttonEnabled = true) }
             sendEffect(ResetPasswordSideEffect.SuccessResetPassword)
         }.onFailure {
             setState { it.copy(isLoading = false, buttonEnabled = true) }
             sendEffect(ResetPasswordSideEffect.FailResetPassword("๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค"))
         }
     }
 }

65-65: Track or address the TODO comment.

The TODO suggests considering better alternatives for the side effect structure. The current sealed class pattern is a standard and appropriate approach for handling one-time events in MVVM. If you have specific concerns about this design, consider documenting them or creating a tracking issue.

Would you like me to open a tracking issue for exploring alternative patterns, or should this TODO be removed if the current approach is satisfactory?

feature/src/dev/kotlin/team/aliens/dms/android/feature/profile/viewmodel/SelectProfileViewModel.kt (1)

116-121: Consider renaming ProfileImageBadRequest for clarity.

The ProfileImageBadRequest side effect is triggered when the URI is null (line 78). A more descriptive name like InvalidUri or NullUri would better communicate the specific error condition.

๐Ÿ“ Suggested rename
 sealed class SelectProfileSideEffect {
     data class SuccessProfileImage(val profileImageUrl: String) : SelectProfileSideEffect()
-    data object ProfileImageBadRequest : SelectProfileSideEffect()
+    data object InvalidUri : SelectProfileSideEffect()
 
     data object FailProfileImage : SelectProfileSideEffect()
 }

Don't forget to update the reference on line 78:

-            sendEffect(SelectProfileSideEffect.ProfileImageBadRequest)
+            sendEffect(SelectProfileSideEffect.InvalidUri)
feature/src/dev/kotlin/team/aliens/dms/android/feature/vote/viewmodel/VoteViewModel.kt (1)

93-112: Consider replacing non-null assertion with safe access.

While the !! operator is technically safe here (the button is only enabled when selectId != null), using explicit null checks improves code resilience and readability.

โ™ป๏ธ Safer alternative using early return
 internal fun postVote() {
-    with(uiState.value) {
-        viewModelScope.launch(Dispatchers.IO) {
-            setState { uiState.value.copy(isLoading = true, buttonEnabled = false) }
-            voteRepository.fetchCreateVotingItem(
-                votingTopicId = uiState.value.vote.id,
-                selectedId = uiState.value.selectId!!,
-            ).onSuccess {
+    val selectedId = uiState.value.selectId ?: return
+    viewModelScope.launch(Dispatchers.IO) {
+        setState { uiState.value.copy(isLoading = true, buttonEnabled = false) }
+        voteRepository.fetchCreateVotingItem(
+            votingTopicId = uiState.value.vote.id,
+            selectedId = selectedId,
+        ).onSuccess {
+            setState { uiState.value.copy(buttonEnabled = false, isLoading = false) }
+            sendEffect(VoteSideEffect.VoteSuccess)
+        }.onFailure {
+            setState { uiState.value.copy(isLoading = false, buttonEnabled = true) }
+            when (it) {
+                is ConflictException -> sendEffect(VoteSideEffect.VoteConflict)
+                else -> sendEffect(VoteSideEffect.VoteFail)
-                setState { uiState.value.copy(buttonEnabled = false, isLoading = false) }
-                sendEffect(VoteSideEffect.VoteSuccess)
-            }.onFailure {
-                setState { uiState.value.copy(isLoading = false, buttonEnabled = true) }
-                when (it) {
-                    is ConflictException -> sendEffect(VoteSideEffect.VoteConflict)
-                    else -> sendEffect(VoteSideEffect.VoteFail)
-                }
             }
         }
     }
+}
-}
feature/src/dev/kotlin/team/aliens/dms/android/feature/vote/navigation/VoteRoute.kt (1)

6-6: Remove unused import.

The java.util.UUID import is not referenced in this file.

feature/src/dev/kotlin/team/aliens/dms/android/feature/vote/ui/VoteScreen.kt (1)

41-41: Simplify redundant nullable cast.

The safe cast operator as? already returns a nullable type, so as? AllVoteSearch? is redundant. Use as? AllVoteSearch instead.

๐Ÿ“œ Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

๐Ÿ“ฅ Commits

Reviewing files that changed from the base of the PR and between 3d147f0 and d5b84fa.

๐Ÿ“’ Files selected for processing (6)
  • app/src/dev/kotlin/team/aliens/dms/android/app/DmsApp.kt
  • feature/src/dev/kotlin/team/aliens/dms/android/feature/profile/viewmodel/SelectProfileViewModel.kt
  • feature/src/dev/kotlin/team/aliens/dms/android/feature/resetpassword/viewmodel/ResetPasswordViewModel.kt
  • feature/src/dev/kotlin/team/aliens/dms/android/feature/vote/navigation/VoteRoute.kt
  • feature/src/dev/kotlin/team/aliens/dms/android/feature/vote/ui/VoteScreen.kt
  • feature/src/dev/kotlin/team/aliens/dms/android/feature/vote/viewmodel/VoteViewModel.kt
๐Ÿงฐ Additional context used
๐Ÿงฌ Code graph analysis (3)
app/src/dev/kotlin/team/aliens/dms/android/app/DmsApp.kt (12)
feature/src/dev/kotlin/team/aliens/dms/android/feature/onboarding/navigation/OnboardingRoute.kt (1)
  • OnboardingRoute (6-13)
feature/src/dev/kotlin/team/aliens/dms/android/feature/signin/navigation/SignInRoute.kt (1)
  • SignInRoute (7-20)
feature/src/dev/kotlin/team/aliens/dms/android/feature/main/application/navigation/ApplicationRoute.kt (1)
  • ApplicationRoute (9-24)
feature/src/dev/kotlin/team/aliens/dms/android/feature/vote/navigation/VoteRoute.kt (1)
  • VoteRoute (8-17)
feature/src/dev/kotlin/team/aliens/dms/android/feature/remain/navigation/RemainApplicationRoute.kt (1)
  • RemainApplicationRoute (7-16)
feature/src/dev/kotlin/team/aliens/dms/android/feature/main/mypage/navigation/MyPageRoute.kt (1)
  • MyPageRoute (8-19)
feature/src/dev/kotlin/team/aliens/dms/android/feature/meal/navigation/MealRoute.kt (1)
  • MealRoute (6-13)
feature/src/dev/kotlin/team/aliens/dms/android/feature/point/navigation/PointHistoryRoute.kt (1)
  • PointHistoryRoute (7-14)
feature/src/dev/kotlin/team/aliens/dms/android/feature/resetpassword/navigation/ResetPasswordRoute.kt (1)
  • ResetPasswordRoute (8-21)
feature/src/dev/kotlin/team/aliens/dms/android/feature/resetpassword/navigation/CheckPasswordRoute.kt (1)
  • CheckPasswordRoute (7-18)
feature/src/dev/kotlin/team/aliens/dms/android/feature/profile/route/SelectProfileRoute.kt (1)
  • SelectProfileRoute (7-18)
feature/src/dev/kotlin/team/aliens/dms/android/feature/profile/route/AdjustProfileRoute.kt (1)
  • AdjustProfileRoute (8-19)
feature/src/dev/kotlin/team/aliens/dms/android/feature/resetpassword/viewmodel/ResetPasswordViewModel.kt (2)
core/ui/src/dev/java/team/aliens/dms/android/core/ui/viewmodel/BaseStateViewModel.kt (2)
  • setState (29-31)
  • sendEffect (33-35)
shared/validator/src/main/java/team/aliens/dms/android/shared/validator/PasswordValidator.kt (1)
  • checkIfPasswordValid (11-11)
feature/src/dev/kotlin/team/aliens/dms/android/feature/profile/viewmodel/SelectProfileViewModel.kt (1)
core/ui/src/dev/java/team/aliens/dms/android/core/ui/viewmodel/BaseStateViewModel.kt (2)
  • setState (29-31)
  • sendEffect (33-35)
โฐ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
๐Ÿ”‡ Additional comments (11)
app/src/dev/kotlin/team/aliens/dms/android/app/DmsApp.kt (4)

103-114: LGTM!

The refactoring to accept Boolean instead of StateFlow<Boolean> simplifies this composable and moves state collection responsibility to the caller. The LaunchedEffect dependencies are correctly specified.


243-247: LGTM!

The navigation entry correctly captures the pointType from the NavKey and uses it to reconstruct the key for removal. Data class equality ensures the correct entry is removed.


248-260: LGTM!

The navigation logic for ResetPasswordScreenNav correctly handles the back navigation by removing both the current screen and CheckPasswordScreenNav when navigating back to settings, ensuring a clean navigation state.


279-286: LGTM!

The AdjustProfileRoute entry is correctly wired with the model parameter and navigation callbacks.

feature/src/dev/kotlin/team/aliens/dms/android/feature/resetpassword/viewmodel/ResetPasswordViewModel.kt (2)

11-14: LGTM!

The ViewModel is properly structured with Hilt dependency injection, and the userRepository is correctly marked as private following encapsulation best practices.


16-24: LGTM!

The password setter methods correctly update state and re-evaluate button enablement.

feature/src/dev/kotlin/team/aliens/dms/android/feature/profile/viewmodel/SelectProfileViewModel.kt (3)

1-21: LGTM: Clean setup with proper DI configuration.

The imports, class declaration, and dependency injection setup are well-structured. The use of @ApplicationContext for Context injection and internal visibility for the ViewModel are appropriate.


23-26: LGTM: Efficient guard for image loading.

The guard check prevents unnecessary reloading of images when the list is already populated.


52-64: LGTM: Correct toggle logic for image selection.

The selection and deselection logic properly maintains the selectedUri and enabled state flags.

feature/src/dev/kotlin/team/aliens/dms/android/feature/vote/viewmodel/VoteViewModel.kt (1)

31-42: LGTM! Type-safe refactoring.

The migration from String to LocalDateTime for time parameters improves type safety and eliminates potential parsing errors. The addition of id assignment in the vote copy aligns with the new result-driven initialization pattern.

feature/src/dev/kotlin/team/aliens/dms/android/feature/vote/navigation/VoteRoute.kt (1)

9-17: LGTM! Clean API simplification.

Removing the explicit parameters in favor of the result store pattern reduces coupling and simplifies the route signature.

Comment on lines 28 to 50
internal fun getUriImage() {
val uriList = mutableListOf<String>()
val contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
val projection = arrayOf(MediaStore.Images.Media._ID)

val cursor = context.contentResolver.query(
contentUri,
projection,
null,
null,
"${MediaStore.Images.Media.DATE_ADDED} DESC"
)

cursor?.use {
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
while (cursor.moveToNext()) {
val id = cursor.getLong(idColumn)
val imageUri = ContentUris.withAppendedId(contentUri, id)
uriList.add(imageUri.toString())
}
}
setState { it.copy(uriList = uriList) }
}
Copy link

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue | ๐ŸŸ  Major

Move MediaStore query to IO dispatcher.

The ContentResolver.query operation can be slow when there are many images in the gallery, potentially blocking the calling thread. Since loadImagesIfNeeded() may be invoked from the UI thread, this could cause UI jank.

๐Ÿ”„ Recommended refactor to use IO dispatcher
     internal fun getUriImage() {
-        val uriList = mutableListOf<String>()
-        val contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
-        val projection = arrayOf(MediaStore.Images.Media._ID)
-
-        val cursor = context.contentResolver.query(
-            contentUri,
-            projection,
-            null,
-            null,
-            "${MediaStore.Images.Media.DATE_ADDED} DESC"
-        )
-
-        cursor?.use {
-            val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
-            while (cursor.moveToNext()) {
-                val id = cursor.getLong(idColumn)
-                val imageUri = ContentUris.withAppendedId(contentUri, id)
-                uriList.add(imageUri.toString())
+        viewModelScope.launch(Dispatchers.IO) {
+            val uriList = mutableListOf<String>()
+            val contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
+            val projection = arrayOf(MediaStore.Images.Media._ID)
+
+            val cursor = context.contentResolver.query(
+                contentUri,
+                projection,
+                null,
+                null,
+                "${MediaStore.Images.Media.DATE_ADDED} DESC"
+            )
+
+            cursor?.use {
+                val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
+                while (cursor.moveToNext()) {
+                    val id = cursor.getLong(idColumn)
+                    val imageUri = ContentUris.withAppendedId(contentUri, id)
+                    uriList.add(imageUri.toString())
+                }
             }
+            setState { it.copy(uriList = uriList) }
         }
-        setState { it.copy(uriList = uriList) }
     }
๐Ÿค– Prompt for AI Agents
In
@feature/src/dev/kotlin/team/aliens/dms/android/feature/profile/viewmodel/SelectProfileViewModel.kt
around lines 28 - 50, The MediaStore query in getUriImage blocks the calling
thread; wrap the contentResolver.query and cursor iteration inside a background
IO coroutine using withContext(Dispatchers.IO) so the heavy I/O runs off the UI
thread, then post the resulting uriList back to the main thread (or call
setState from the coroutine after switching back) to update state; specifically
modify getUriImage (and any callers like loadImagesIfNeeded) to perform the
query + cursor.use inside Dispatchers.IO and only update setState on the main
dispatcher.

Comment on lines +40 to +49
snapshotFlow {
resultStore.resultStateMap["vote_result"]?.value as? AllVoteSearch?
}.collect { result ->
if (result != null) {
viewModel.initState(result.topicName, result.startTime, result.endTime, result.id)
resultStore.removeResult<String?>(resultKey = "vote_result")
} else {
onShowSnackBar(DmsSnackBarType.ERROR, "์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค์ง€ ๋ชป ํ–ˆ์–ด์š”")
}
}
Copy link

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue | ๐Ÿ”ด Critical

Fix initialization error handling logic.

The snapshotFlow emits immediately upon collection. If the result store doesn't contain a value at "vote_result" when the screen first loads, it will emit null and trigger the error snackbar on Line 47, even though no actual error occurred.

This causes the error message "์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค์ง€ ๋ชป ํ–ˆ์–ด์š”" to appear on every initial navigation to this screen.

Consider one of these approaches:

  1. Check if the result exists before starting collection
  2. Use a flag to track whether initialization has been attempted
  3. Handle the initial null state differently from an error state
๐Ÿ› ๏ธ Suggested fix using initial state check
 LaunchedEffect(Unit) {
+    val initialResult = resultStore.resultStateMap["vote_result"]?.value as? AllVoteSearch
+    if (initialResult == null) {
+        onShowSnackBar(DmsSnackBarType.ERROR, "์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค์ง€ ๋ชป ํ–ˆ์–ด์š”")
+        return@LaunchedEffect
+    }
+    
+    viewModel.initState(
+        initialResult.topicName,
+        initialResult.startTime,
+        initialResult.endTime,
+        initialResult.id
+    )
+    resultStore.removeResult<AllVoteSearch>(resultKey = "vote_result")
-    snapshotFlow {
-        resultStore.resultStateMap["vote_result"]?.value as? AllVoteSearch?
-    }.collect { result ->
-        if (result != null) {
-            viewModel.initState(result.topicName, result.startTime, result.endTime, result.id)
-            resultStore.removeResult<String?>(resultKey = "vote_result")
-        } else {
-            onShowSnackBar(DmsSnackBarType.ERROR, "์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค์ง€ ๋ชป ํ–ˆ์–ด์š”")
-        }
-    }
-
-
     viewModel.sideEffect.collect {

Committable suggestion skipped: line range outside the PR's diff.

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

๐Ÿค– Fix all issues with AI agents
In
@feature/src/dev/kotlin/team/aliens/dms/android/feature/profile/viewmodel/SelectProfileViewModel.kt:
- Around line 84-106: The call to fileRepository.fetchPresignedUrl(file.name) in
SelectProfileViewModel.fetchPresignedUrl is outside the runCatching block so any
exception from fetchPresignedUrl will crash the coroutine; move the
fetchPresignedUrl call inside a single runCatching that wraps both fetching the
presignedInfo and the subsequent fileRepository.uploadFile call, then keep the
existing .onSuccess { setState { ... };
sendEffect(SelectProfileSideEffect.SuccessProfileImage(...)) } and .onFailure {
sendEffect(SelectProfileSideEffect.FailProfileImage) } handlers so any exception
from either fetch or upload is caught and handled.
- Around line 68-82: uploadProfileImage currently calls
team.aliens.dms.android.core.file.File.toFile(...) on the calling thread and
without error handling; wrap the blocking call in a coroutine using
Dispatchers.IO (e.g., viewModelScope.launch or withContext(Dispatchers.IO)) and
surround the toFile call with try/catch to handle exceptions (invalid URI,
inaccessible file) โ€” on error
sendEffect(SelectProfileSideEffect.ProfileImageBadRequest) or an appropriate
error effect, and on success switch back to Main to setState { it.copy(uri =
uri) } and call fetchPresignedUrl(file = ...) so UI updates and network flow
remain on the correct dispatcher.
๐Ÿ“œ Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

๐Ÿ“ฅ Commits

Reviewing files that changed from the base of the PR and between d5b84fa and 1271bcb.

๐Ÿ“’ Files selected for processing (1)
  • feature/src/dev/kotlin/team/aliens/dms/android/feature/profile/viewmodel/SelectProfileViewModel.kt
๐Ÿงฐ Additional context used
๐Ÿงฌ Code graph analysis (1)
feature/src/dev/kotlin/team/aliens/dms/android/feature/profile/viewmodel/SelectProfileViewModel.kt (1)
core/ui/src/dev/java/team/aliens/dms/android/core/ui/viewmodel/BaseStateViewModel.kt (2)
  • setState (29-31)
  • sendEffect (33-35)
โฐ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
๐Ÿ”‡ Additional comments (1)
feature/src/dev/kotlin/team/aliens/dms/android/feature/profile/viewmodel/SelectProfileViewModel.kt (1)

28-52: LGTM! MediaStore query is now properly dispatched.

The MediaStore query and cursor iteration are now correctly wrapped in Dispatchers.IO, which resolves the previous concern about blocking the calling thread.

Comment on lines +68 to +82
internal fun uploadProfileImage(
uri: Uri?,
) {
if (uri != null) {
setState { it.copy(uri = uri) }
fetchPresignedUrl(
file = team.aliens.dms.android.core.file.File.toFile(
context = context,
uri = uri,
),
)
} else {
sendEffect(SelectProfileSideEffect.ProfileImageBadRequest)
}
}
Copy link

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue | ๐ŸŸ  Major

File.toFile() blocks the calling thread and lacks error handling.

Two issues remain in this function:

  1. Blocking I/O (previously flagged): Line 74 calls File.toFile(context, uri) synchronously, which can block the calling thread and cause UI jank or ANR.

  2. Missing error handling: File.toFile() can throw exceptions (e.g., invalid URI, inaccessible file) which will crash the coroutine uncaught.

๐Ÿ”„ Recommended fix to move to IO dispatcher and add error handling
     internal fun uploadProfileImage(
         uri: Uri?,
     ) {
         if (uri != null) {
             setState { it.copy(uri = uri) }
-            fetchPresignedUrl(
-                file = team.aliens.dms.android.core.file.File.toFile(
-                    context = context,
-                    uri = uri,
-                ),
-            )
+            viewModelScope.launch(Dispatchers.IO) {
+                runCatching {
+                    team.aliens.dms.android.core.file.File.toFile(
+                        context = context,
+                        uri = uri,
+                    )
+                }.onSuccess { file ->
+                    fetchPresignedUrl(file)
+                }.onFailure {
+                    sendEffect(SelectProfileSideEffect.FailProfileImage)
+                }
+            }
         } else {
             sendEffect(SelectProfileSideEffect.ProfileImageBadRequest)
         }
     }
๐Ÿ“ 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
internal fun uploadProfileImage(
uri: Uri?,
) {
if (uri != null) {
setState { it.copy(uri = uri) }
fetchPresignedUrl(
file = team.aliens.dms.android.core.file.File.toFile(
context = context,
uri = uri,
),
)
} else {
sendEffect(SelectProfileSideEffect.ProfileImageBadRequest)
}
}
internal fun uploadProfileImage(
uri: Uri?,
) {
if (uri != null) {
setState { it.copy(uri = uri) }
viewModelScope.launch(Dispatchers.IO) {
runCatching {
team.aliens.dms.android.core.file.File.toFile(
context = context,
uri = uri,
)
}.onSuccess { file ->
fetchPresignedUrl(file)
}.onFailure {
sendEffect(SelectProfileSideEffect.FailProfileImage)
}
}
} else {
sendEffect(SelectProfileSideEffect.ProfileImageBadRequest)
}
}
๐Ÿค– Prompt for AI Agents
In
@feature/src/dev/kotlin/team/aliens/dms/android/feature/profile/viewmodel/SelectProfileViewModel.kt
around lines 68 - 82, uploadProfileImage currently calls
team.aliens.dms.android.core.file.File.toFile(...) on the calling thread and
without error handling; wrap the blocking call in a coroutine using
Dispatchers.IO (e.g., viewModelScope.launch or withContext(Dispatchers.IO)) and
surround the toFile call with try/catch to handle exceptions (invalid URI,
inaccessible file) โ€” on error
sendEffect(SelectProfileSideEffect.ProfileImageBadRequest) or an appropriate
error effect, and on success switch back to Main to setState { it.copy(uri =
uri) } and call fetchPresignedUrl(file = ...) so UI updates and network flow
remain on the correct dispatcher.

Comment on lines +84 to +106
internal fun fetchPresignedUrl(
file: File,
) {
viewModelScope.launch(Dispatchers.IO) {
val presignedInfo = fileRepository.fetchPresignedUrl(file.name)
runCatching {
fileRepository.uploadFile(
presignedUrl = presignedInfo.fileUploadUrl,
file = file,
)
}.onSuccess { fileUrl ->
setState {
it.copy(
profileImageUrl = fileUrl.fileUrl,
buttonEnabled = true,
)
}
sendEffect(SelectProfileSideEffect.SuccessProfileImage(fileUrl.fileUrl))
}.onFailure {
sendEffect(SelectProfileSideEffect.FailProfileImage)
}
}
}
Copy link

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue | ๐Ÿ”ด Critical

CRITICAL: fetchPresignedUrl exception will crash the coroutine.

Line 88 calls fileRepository.fetchPresignedUrl(file.name) outside the runCatching block. Any exception thrown by this call will crash the coroutine without being caught, and the user won't receive the FailProfileImage side effect.

This critical issue was flagged in previous reviews and remains unfixed.

๐Ÿ”’ Proposed fix to move fetchPresignedUrl inside runCatching
     internal fun fetchPresignedUrl(
         file: File,
     ) {
         viewModelScope.launch(Dispatchers.IO) {
-            val presignedInfo = fileRepository.fetchPresignedUrl(file.name)
             runCatching {
+                val presignedInfo = fileRepository.fetchPresignedUrl(file.name)
                 fileRepository.uploadFile(
                     presignedUrl = presignedInfo.fileUploadUrl,
                     file = file,
                 )
             }.onSuccess { fileUrl ->
                 setState {
                     it.copy(
                         profileImageUrl = fileUrl.fileUrl,
                         buttonEnabled = true,
                     )
                 }
                 sendEffect(SelectProfileSideEffect.SuccessProfileImage(fileUrl.fileUrl))
             }.onFailure {
                 sendEffect(SelectProfileSideEffect.FailProfileImage)
             }
         }
     }
๐Ÿค– Prompt for AI Agents
In
@feature/src/dev/kotlin/team/aliens/dms/android/feature/profile/viewmodel/SelectProfileViewModel.kt
around lines 84 - 106, The call to fileRepository.fetchPresignedUrl(file.name)
in SelectProfileViewModel.fetchPresignedUrl is outside the runCatching block so
any exception from fetchPresignedUrl will crash the coroutine; move the
fetchPresignedUrl call inside a single runCatching that wraps both fetching the
presignedInfo and the subsequent fileRepository.uploadFile call, then keep the
existing .onSuccess { setState { ... };
sendEffect(SelectProfileSideEffect.SuccessProfileImage(...)) } and .onFailure {
sendEffect(SelectProfileSideEffect.FailProfileImage) } handlers so any exception
from either fetch or upload is caught and handled.

@uson1004 uson1004 merged commit 6342d77 into develop Jan 7, 2026
1 of 2 checks passed
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.

๋งˆ์ดํŽ˜์ด์ง€ ๊ตฌํ˜„

2 participants