Skip to content

πŸ”€ :: (#818) μ•Œλ¦Ό 및 곡지 ν™”λ©΄ κ΅¬ν˜„#819

Merged
uson1004 merged 57 commits intodevelopfrom
feature/818-μ•Œλ¦Ό-및-곡지-ν™”λ©΄-κ΅¬ν˜„
Jan 19, 2026

Hidden character warning

The head ref may contain hidden characters: "feature/818-\uc54c\ub9bc-\ubc0f-\uacf5\uc9c0-\ud654\uba74-\uad6c\ud604"
Merged

πŸ”€ :: (#818) μ•Œλ¦Ό 및 곡지 ν™”λ©΄ κ΅¬ν˜„#819
uson1004 merged 57 commits intodevelopfrom
feature/818-μ•Œλ¦Ό-및-곡지-ν™”λ©΄-κ΅¬ν˜„

Conversation

@uson1004
Copy link
Member

@uson1004 uson1004 commented Jan 14, 2026

κ°œμš”

μ•Œλ¦Ό 및 곡지 화면을 κ΅¬ν˜„ν•˜μ˜€μŠ΅λ‹ˆλ‹€.
κ·Έ μ™Έ μžμž˜ν•œ μˆ˜μ •μ‚¬ν•­λ“€μ„ μ•ˆμ •ν™” μ‹œμΌ°μŠ΅λ‹ˆλ‹€

μž‘μ—…μ‚¬ν•­

    image image

μΆ”κ°€ 둜 ν•  말

Summary by CodeRabbit

  • New Features

    • Notification center with tabs, read-status updates, and navigation to notification details.
    • Notice detail screen for viewing full announcements.
    • Point history supports category-based entry.
  • Improvements

    • Safer UI insets applied across many screens (system/navigation bar padding).
    • Sign-in requests runtime notification permission on supported devices.
    • Date chips are tappable; calendar interaction improved.
    • Updated notification/notice icons, layered button background, date/time formatting, and refined animations.

✏️ Tip: You can customize this high-level summary in your review settings.

@uson1004 uson1004 requested a review from a team January 14, 2026 01:49
@uson1004 uson1004 self-assigned this Jan 14, 2026
@uson1004 uson1004 added the feat μƒˆλ‘œμš΄ κΈ°λŠ₯을 μΆ”κ°€ ν•  경우 label Jan 14, 2026
@uson1004 uson1004 linked an issue Jan 14, 2026 that may be closed by this pull request
@coderabbitai
Copy link

coderabbitai bot commented Jan 14, 2026

πŸ“ Walkthrough

Walkthrough

Adds Notice and Notification features (UI, viewmodels, repositories, network endpoints), migrates many network/data sources and repositories to Result-wrapped return types using suspendRunCatching/runCatching, applies system-bar insets across many screens, updates design assets/icons, and adds date/time helpers and logging.

Changes

Cohort / File(s) Summary
Manifest
app/src/dev/AndroidManifest.xml
Added androidx.startup.InitializationProvider provider entry; removed nested androidx.work.WorkManagerInitializer meta-data inside that provider.
App / Navigation
app/src/dev/kotlin/.../DmsApp.kt, .../MainActivity.kt, .../MainActivityViewModel.kt
Removed isJwtAvailable parameter from DmsApp signature (now derived from ViewModel); added NotificationScreenNav and NoticeDetailScreenNav NavKeys; adjusted MainActivityViewModel constructor visibility for jwtProvider.
Notification feature (new)
feature/src/dev/.../notification/navigation/NotificationRoute.kt, .../ui/NotificationScreen.kt, .../viewmodel/NotificationViewModel.kt, data/.../notification/*, network/.../notification/*
New NotificationRoute, NotificationScreen UI and ViewModel; repository and network changes to fetch/partition notifications, update read status, and support Result flows.
Notice feature (new)
feature/src/dev/.../notice/*, data/src/dev/.../notice/*, network/.../notice/*
New Notice domain model, mapper, NoticeRepository/Impl, NoticeDetail UI and NoticeDetailViewModel; LocalResultStore usage to pass selected notice id to detail route.
Network: Result & suspendRunCatching migration
network/src/dev/.../*Datasource*.kt, ...*DatasourceImpl*.kt
Many network datasources migrated from handleNetworkRequest/Response to suspendRunCatching/runCatching and updated to return Result<T> across auth, file, meal, notice, notification, point, remains, student, user, voting, etc.
Repository adaptations
data/src/dev/.../*Repository*.kt, ...*RepositoryImpl*.kt
Repositories updated to consume/produce Result<T> or new response models; mapping via .map/onSuccess used; added updateNotificationReadStatus.
UI insets & navigation callbacks
feature/src/dev/.../home/*, .../mypage/*, .../meal/*, .../point/*, .../signin/*, .../profile/*, .../remain/*, .../resetpassword/*, .../setting/*, .../vote/*
Applied systemBarsPadding/navigationBarsPadding/imePadding to many screens; introduced and propagated onNavigateNotification/onNavigateNoticeDetail callbacks; SignIn handles POST_NOTIFICATIONS permission on Android 13+.
Design system & assets
core/design-system/src/dev/.../foundation/DmsIcon.kt, .../drawable/ic_notice.xml, .../drawable/ic_notification.xml, .../button/Button.kt, .../calendar/DmsCalendar.kt, .../setting/component/SettingRotateContent.kt
Icon swap/add (Notification/Notice), new vector drawable, DmsLayeredButton default background and loading default changed, increased calendar day padding, rotation animation now 180Β° tween.
Date/time & elapsed helpers
core/ui/src/dev/.../util/Date.kt, shared/date/src/main/.../Extensions.kt
Added LocalDateTime.toDateTimeString() and LocalDateTime.toElapsedText(now) helpers for formatted datetime and elapsed text.
JWT, stores & logging
core/jwt/.../JwtProviderImpl.kt, .../JwtStoreImpl.kt, core/device/*, shared/exception/*
Added Android logging in Jwt flows; wrapped token persistence with runCatching; device datastore/store methods made suspend and return Result<T>; added suspendRunCatching utility.
Misc / small fixes
app/src/dev/.../BottomNavigationBar.kt, .../BottomMenu.kt, build files, many composable small edits
Removed unused imports, made DateChip clickable, added module dependency on Shared.EXCEPTION, minor layout and signature tweaks across multiple composables.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant NotificationScreen
  participant NotificationViewModel
  participant NotificationRepository
  participant NetworkNotificationDataSource
  participant NotificationApi

  User->>NotificationScreen: open notifications
  NotificationScreen->>NotificationViewModel: loadNotifications()
  NotificationViewModel->>NotificationRepository: fetchNotifications()
  NotificationRepository->>NetworkNotificationDataSource: fetchNotifications()
  NetworkNotificationDataSource->>NotificationApi: GET /notifications
  NotificationApi-->>NetworkNotificationDataSource: Response (list)
  NetworkNotificationDataSource-->>NotificationRepository: Result<FetchNotificationsResponse>
  NotificationRepository-->>NotificationViewModel: Result<List<Notification>>
  NotificationViewModel-->>NotificationScreen: update uiState (notifications/notices)
Loading
sequenceDiagram
  participant User
  participant NotificationScreen
  participant NotificationViewModel
  participant NotificationRepository
  participant NetworkNotificationDataSource
  participant NotificationApi
  participant LocalResultStore

  User->>NotificationScreen: tap notification item (UUID)
  NotificationScreen->>NotificationViewModel: onNotificationClick(UUID)
  NotificationViewModel->>NotificationRepository: updateNotificationReadStatus(UUID)
  NotificationRepository->>NetworkNotificationDataSource: updateNotificationReadStatus(UUID)
  NetworkNotificationDataSource->>NotificationApi: PATCH /notifications/{id}/read
  NotificationApi-->>NetworkNotificationDataSource: Response<Unit>
  NetworkNotificationDataSource-->>NotificationRepository: Result<Unit>
  NotificationRepository-->>NotificationViewModel: Result<Unit>
  NotificationViewModel->>NotificationViewModel: refresh notifications
  NotificationViewModel->>LocalResultStore: store notice_detail_result (UUID)
  NotificationScreen->>User: navigate to NoticeDetailRoute (reads LocalResultStore)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • parkuiery

"🐰
I hopped through code with eager paws,
Bells and notices, new Result-wrapped laws,
Insets snug, icons bright in place,
I left a nibble in each file's space,
Hop, click, and navigate β€” cheer the race!"

πŸš₯ Pre-merge checks | βœ… 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
βœ… 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 clearly describes the main implementation objective: notification and notice screens (μ•Œλ¦Ό 및 곡지 ν™”λ©΄ κ΅¬ν˜„). It directly relates to the substantial feature additions in the changeset.

✏️ 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.

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

Note

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

Caution

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

⚠️ Outside diff range comments (5)
feature/src/dev/kotlin/team/aliens/dms/android/feature/vote/ui/VoteScreen.kt (1)

40-76: Critical: Second collect block is unreachable β€” side effects will never be handled.

The snapshotFlow { }.collect { } on lines 41-50 is a terminal operator that suspends indefinitely. The viewModel.sideEffect.collect { } block starting at line 53 will never execute, meaning VoteSuccess, VoteConflict, VoteFail, and VoteLoadFail side effects are silently ignored.

Each collect should run in its own coroutine scope:

πŸ› Proposed fix β€” launch separate coroutines for each collector
 LaunchedEffect(Unit) {
-    snapshotFlow {
-        resultStore.resultStateMap["vote_result"]?.value as? AllVoteSearch?
-    }.collect { result ->
-        if (result != null) {
-            viewModel.initState(result)
-            resultStore.removeResult<String?>(resultKey = "vote_result")
-        } else {
-            onShowSnackBar(DmsSnackBarType.ERROR, "정보λ₯Ό κ°€μ Έμ˜€μ§€ λͺ» ν–ˆμ–΄μš”")
+    launch {
+        snapshotFlow {
+            resultStore.resultStateMap["vote_result"]?.value as? AllVoteSearch?
+        }.collect { result ->
+            if (result != null) {
+                viewModel.initState(result)
+                resultStore.removeResult<String?>(resultKey = "vote_result")
+            } else {
+                onShowSnackBar(DmsSnackBarType.ERROR, "정보λ₯Ό κ°€μ Έμ˜€μ§€ λͺ» ν–ˆμ–΄μš”")
+            }
         }
     }
-
-
-    viewModel.sideEffect.collect {
-        when (it) {
-            // ... side effect handling
+    launch {
+        viewModel.sideEffect.collect {
+            when (it) {
+                // ... side effect handling
+            }
         }
     }
 }
app/src/dev/kotlin/team/aliens/dms/android/app/DmsApp.kt (1)

119-130: Missing isJwtAvailable in LaunchedEffect dependencies.

The LaunchedEffect uses isJwtAvailable (line 122) to determine the initial screen, but the effect only depends on isOnboardingCompleted. If isJwtAvailable changes after the initial composition (e.g., token expiration detected), the navigation won't update accordingly.

πŸ› Proposed fix
-    LaunchedEffect(isOnboardingCompleted) {
+    LaunchedEffect(isOnboardingCompleted, isJwtAvailable) {
data/src/dev/kotlin/team.aliens.dms.android.data/student/repository/StudentRepositoryImpl.kt (2)

41-60: Double-wrapping Result leads to incorrect error propagation.

networkStudentDataSource.signUp() already returns Result<SignUpResponse>, so wrapping it in runCatching creates Result<Result<SignUpResponse>>. The current implementation:

  1. Doesn't propagate network failures to the caller (outer runCatching succeeds even when inner Result fails)
  2. Returns Result<Unit> success even when response is a failure
πŸ› Proposed fix
     override suspend fun signUp(
         schoolVerificationCode: String,
         schoolVerificationAnswer: String,
         email: String,
         emailVerificationCode: String,
         grade: Int,
         classRoom: Int,
         number: Int,
         accountId: String,
         password: String,
         profileImageUrl: String?,
-    ): Result<Unit> = runCatching {
-        val response: Result<SignUpResponse> = networkStudentDataSource.signUp(
+    ): Result<Unit> =
+        networkStudentDataSource.signUp(
             request = SignUpRequest(
                 schoolVerificationCode = schoolVerificationCode,
                 schoolVerificationAnswer = schoolVerificationAnswer,
                 email = email,
                 emailVerificationCode = emailVerificationCode,
                 grade = grade,
                 classRoom = classRoom,
                 number = number,
                 accountId = accountId,
                 password = password,
                 profileImageUrl = profileImageUrl,
             ),
-        )
-        response.onSuccess {
+        ).onSuccess {
             jwtProvider.updateTokens(tokens = it.extractTokens())
             schoolProvider.updateFeatures(features = it.extractFeatures())
-        }
-    }
+        }.map { }

108-124: Same double-wrapping issue as signUp.

networkStudentDataSource.resetPassword() returns Result<Unit>, so the outer runCatching creates Result<Result<Unit>>. Network failures won't propagate correctly.

πŸ› Proposed fix
     override suspend fun resetPassword(
         accountId: String,
         studentName: String,
         email: String,
         emailVerificationCode: String,
         newPassword: String,
-    ): Result<Unit> = runCatching {
-        networkStudentDataSource.resetPassword(
+    ): Result<Unit> =
+        networkStudentDataSource.resetPassword(
             ResetPasswordRequest(
                 accountId = accountId,
                 studentName = studentName,
                 email = email,
                 emailVerificationCode = emailVerificationCode,
                 newPassword = newPassword,
             ),
         )
-    }
feature/src/dev/kotlin/team/aliens/dms/android/feature/profile/ui/SelectProfileScreen.kt (1)

73-77: Potential logic inconsistency: ProfileImageBadRequest shows success snackbar.

The side effect ProfileImageBadRequest (implying an error) triggers a SUCCESS snackbar with "μ—…λ‘œλ“œ 성곡!" (Upload success). This appears contradictoryβ€”either rename the side effect to reflect success, or change the snackbar type to ERROR if this is truly an error case.

πŸ€– Fix all issues with AI agents
In `@app/src/dev/kotlin/team/aliens/dms/android/app/DmsApp.kt`:
- Around line 189-194: The onNavigateNotice callback currently just pushes
NoticeDetailScreenNav without storing the notice ID in resultStore, so
NoticeDetailScreen cannot read the ID; update the HomeRoute callback signature
and its invocations so onNavigateNotice accepts a UUID (change from () -> Unit
to (UUID) -> Unit), then before adding NoticeDetailScreenNav call
resultStore.setResult<UUID?>("notice_result", theId); mirror the pattern used in
onNavigateNotificationDetailClick (which calls
resultStore.setResult<UUID?>("notice_result", it) then
backStack.add(NotificationScreenNav)) and update any callers to pass the
selected notice ID when invoking onNavigateNotice.

In `@core/jwt/src/main/java/team/aliens/dms/android/core/jwt/JwtProviderImpl.kt`:
- Around line 67-73: The onSuccess block in JwtProviderImpl currently logs
sensitive token data (tokens.refreshToken) and the onFailure block silently
swallows exceptions; remove the Log.d call so no token values are written to
logs and instead keep assigning this@JwtProviderImpl._cachedAccessToken and
_cachedRefreshToken as before, and implement proper failure handling in the
onFailure handler by either logging a non-sensitive error message with the
caught exception (without printing token values) or rethrowing/propagating the
exception so callers can handle it; update references to the onSuccess/onFailure
handlers in JwtProviderImpl accordingly and ensure no other debug logging prints
token contents.

In `@core/ui/src/dev/java/team/aliens/dms/android/core/ui/util/Date.kt`:
- Around line 9-10: The toDateTimeString() implementation in LocalDateTime
incorrectly classifies noon and doesn't convert to 12‑hour clock: change the
AM/PM condition to use hour >= 12 to mark PM for 12:00 and convert the hour
value to 12‑hour format using a formula like adjustedHour = if (hour % 12 == 0)
12 else hour % 12 before inserting into the string; update
LocalDateTime.toDateTimeString() to use the adjustedHour for display (optionally
zero‑pad minute if desired) or replace the whole function with a
DateTimeFormatter-based solution for locale-aware formatting.

In
`@data/src/dev/kotlin/team.aliens.dms.android.data/meal/repository/MealRepositoryImpl.kt`:
- Around line 22-27: The current updateMeal implementation uses Result.map so
exceptions thrown inside the transform (like throwing CannotFindMealException
when find returns null or any exception from databaseMealDataSource.saveMeals)
escape instead of being wrapped; change the call to use mapCatching on the
Result returned by networkMealDataSource.fetchMeals(date) and perform the same
transform inside its lambda so any thrown exceptions (including from saveMeals
or the explicit CannotFindMealException) are captured as Result.failure, keeping
the rest of the logic (response.toModel(), saveMeals(meals.toEntity()), and
finding the specific date) unchanged.

In
`@data/src/dev/kotlin/team.aliens.dms.android.data/student/repository/StudentRepository.kt`:
- Around line 7-8: StudentRepository currently exposes network-layer types
(ExamineStudentNumberResponse, FindIdResponse) in its abstract interface,
leaking implementation details; change the abstract methods in StudentRepository
back to return the domain models (e.g., StudentName, HashedEmail or other domain
DTOs) and perform mapping from ExamineStudentNumberResponse/FindIdResponse to
those domain types inside the concrete implementation (the class implementing
StudentRepository), ensuring only domain types appear in the repository API.

In
`@data/src/dev/kotlin/team.aliens.dms.android.data/student/repository/StudentRepositoryImpl.kt`:
- Around line 140-143: The caches are being cleared before the network call, so
move jwtProvider.clearCaches() and schoolProvider.clearCaches() to execute only
after networkStudentDataSource.withdraw() succeeds: call
networkStudentDataSource.withdraw(), check its Result (or catch exceptions) and
only when it indicates success perform jwtProvider.clearCaches() and
schoolProvider.clearCaches(), then return the successful Result; if the network
call fails, return the failure without clearing caches.

In
`@feature/src/dev/kotlin/team/aliens/dms/android/feature/notice/ui/NoticeDetailScreen.kt`:
- Around line 46-57: The code is removing the wrong result key and treating the
initial null emission as an error; change the remove call to
removeResult(resultKey = "notice_result") to match the read from
resultStateMap["notice_result"], and prevent the immediate null-triggered
snackbar by ignoring initial null emissions β€” for example, use snapshotFlow {
resultStore.resultStateMap["notice_result"]?.value as? UUID? }.filterNotNull()
(or check for presence of the key before showing the snackbar / use a first-time
flag or timeout) inside LaunchedEffect so only real results trigger
viewModel.getNotificationDetail(...) and removeResult(...), and only unexpected
nulls after a valid state produce onShowSnackBar.

In
`@feature/src/dev/kotlin/team/aliens/dms/android/feature/notice/viewmodel/NoticeDetailViewModel.kt`:
- Around line 21-32: The code wraps a Result-returning call in runCatching and
then swallows failures, so remove the redundant runCatching around
noticeRepository.fetchNoticeDetails(noticeId) and handle the Result directly
(e.g., call fetchNoticeDetails(...).onSuccess { notice -> build
NotificationDetailUi and call setState { it.copy(notice = noticeUi) }
}.onFailure { error -> update the ViewModel state to reflect the failure or
populate an error field so the UI can show feedback, or emit an
event/side-effect for an error dialog). Keep references to
NoticeDetailViewModel, fetchNoticeDetails, NotificationDetailUi, and setState
when making the change.

In
`@feature/src/dev/kotlin/team/aliens/dms/android/feature/notification/ui/NotificationScreen.kt`:
- Around line 123-139: The pager content lambda is using pagerState.currentPage
instead of the lambda's page index, causing offscreen pages to render wrong
content; change the HorizontalPager content lambda to accept the page index
parameter (e.g., use "page" from the lambda) and switch the conditional to "if
(page == 0)" so NotificationItems is rendered for page 0 and NoticeItems for
other pages; update the lambda in HorizontalPager that currently references
pagerState.currentPage to use the provided page parameter instead.

In
`@feature/src/dev/kotlin/team/aliens/dms/android/feature/notification/viewmodel/NotificationViewModel.kt`:
- Around line 52-63: The success/failure handlers in
NotificationViewModel.updateNotificationReadStatus are inverted:
notificationRepository.updateNotificationReadStatus(...).fold currently calls
fetchNotifications() on failure and sends
NotificationSideEffect.SuccessUpdateNotification on failure; swap the handlers
so onSuccess calls sendEffect(NotificationSideEffect.SuccessUpdateNotification)
and then fetchNotifications() (or at least send the success effect), and
onFailure sends an appropriate failure effect (e.g.,
NotificationSideEffect.FailureUpdateNotification) and/or logs the error; update
the fold handlers accordingly and ensure sendEffect and NotificationSideEffect
constants are used consistently.

In
`@feature/src/dev/kotlin/team/aliens/dms/android/feature/profile/viewmodel/SelectProfileViewModel.kt`:
- Around line 105-107: The onFailure lambda for fetchPresignedUrl in
SelectProfileViewModel is empty and swallows errors; update the onFailure
handler inside the fetchPresignedUrl call to log the Throwable (e.g., via Timber
or logger), update the ViewModel state to reflect the error (e.g., set an error
LiveData/StateFlow or call a showError method), and surface a user-visible
message (Toast/Snackbar or UI error state) so the UI can stop any loading
indicator and inform the user; ensure the handler accepts the Throwable
parameter and use the existing error reporting/logging utilities in
SelectProfileViewModel.

In
`@feature/src/dev/kotlin/team/aliens/dms/android/feature/signin/ui/SigninScreen.kt`:
- Around line 140-149: The Find ID and Reset Password handlers are not wired:
update the SignInScreen signature to accept navigateToFindId and
navigateToFindPassword, propagate those two callbacks into the SignIn composable
call, and pass them into UserInformationInputs instead of the empty lambdas;
specifically modify SignInScreen, the SignIn(...) invocation, and the
UserInformationInputs(...) parameters so onFindId uses navigateToFindId and
onResetPassword uses navigateToFindPassword.

In
`@network/src/dev/kotlin/team.aliens.dms.android.network/auth/datasource/NetworkAuthDataSourceImpl.kt`:
- Around line 12-30: The suspend overrides (signIn, sendEmailVerificationCode,
checkEmailVerificationCode, checkIdExists) use Kotlin's runCatching which
swallows CancellationException and breaks coroutine cancellation; replace
runCatching with a cancellation-aware wrapper (e.g., a runSuspendCatching that
rethrows CancellationException and wraps other exceptions into Result.failure)
and call that wrapper from each of these methods (or explicitly catch exceptions
and rethrow CancellationException) so cancellation propagates correctly while
still returning Result<T>.

In
`@network/src/dev/kotlin/team.aliens.dms.android.network/notification/datasource/NetworkNotificationDataSource.kt`:
- Line 33: The method signature updateNotificationReadStatus currently returns a
nullable Result<Unit>? which introduces an invalid third state; change its
return type to non-nullable Result<Unit>, and inside the implementation convert
any nullable API Response<Unit>? into a concrete Result by mapping a null
response to Result.success(Unit) (or a meaningful Result.failure when
appropriate) instead of returning null; ensure the runCatching { ... } block
(and any usage of the API call within updateNotificationReadStatus) always
returns a non-null Result and remove cascading nullable handling in callers.

In
`@network/src/dev/kotlin/team.aliens.dms.android.network/remains/datasource/NetworkRemainsDataSourceImpl.kt`:
- Around line 12-22: The current suspend functions updateRemainsOption,
fetchAppliedRemainsOption, fetchRemainsApplicationTime and fetchRemainsOptions
use runCatching which will swallow CancellationException and break structured
concurrency; replace runCatching with a coroutine-safe wrapper (e.g., a
runSuspendCatching that rethrows CancellationException and returns
Result.success/failure) and call that wrapper inside each of those methods
instead of runCatching; extract the runSuspendCatching utility to a shared util
file used across data sources (auth, user, student, notification, meal, remains)
and update all usages to avoid swallowing cancellations.

In
`@network/src/dev/kotlin/team.aliens.dms.android.network/voting/datasource/NetworkVotingDataSourceImpl.kt`:
- Around line 20-24: The current implementation of
NetworkVotingDataSourceImpl.fetchCreateVotingItem double-wraps the API result by
calling votingApiService.fetchCreateVotingItem(...) inside runCatching and then
getOrThrow(), which rewraps exceptions; instead, return the API Result directly
and flatten nulls like fetchDeleteVotingItem does β€” call
votingApiService.fetchCreateVotingItem(votingTopicId, selectedId)?.map { Unit }
?: Result.success(Unit) so you don't re-wrap errors and maintain consistency
with fetchDeleteVotingItem.
🟑 Minor comments (15)
feature/src/dev/kotlin/team/aliens/dms/android/feature/meal/ui/MealScreen.kt-153-154 (1)

153-154: Redundant padding: navigationBarsPadding() is already included in systemBarsPadding().

systemBarsPadding() applies insets for both status bar and navigation bar. Adding navigationBarsPadding() afterward doubles the bottom padding.

Suggested fix
         modifier = Modifier
             .fillMaxSize()
             .background(DmsTheme.colorScheme.background)
             .systemBarsPadding()
-            .navigationBarsPadding()
             .verticalScroll(rememberScrollState()),
core/jwt/src/main/java/team/aliens/dms/android/core/jwt/JwtProviderImpl.kt-44-46 (1)

44-46: Remove debug logging before merge.

The Log.d("TEST ...") statement is a development artifact that should be removed before merging to develop.

            if (_cachedRefreshToken!!.isExpired()) {
-                Log.d("TEST _cachedRefreshToken!!.isExpired()", _cachedRefreshToken!!.isExpired().toString())
                throw CannotUseRefreshTokenException()
            }
core/jwt/src/main/java/team/aliens/dms/android/core/jwt/JwtProviderImpl.kt-81-85 (1)

81-85: Remove debug logging statement.

The Log.d("TEST", ...) is a debug artifact that should be removed. If error logging is needed for production, use a more descriptive tag and appropriate log level.

Proposed fix
        CoroutineScope(Dispatchers.Default).launch {
            runCatching {
                jwtDataStoreDataSource.storeTokens(tokens = tokens)
-            }.onFailure { Log.d("TEST", it.message.toString()) }
+            }.onFailure { exception ->
+                // Use proper logging if needed:
+                // Log.e("JwtProvider", "Failed to store tokens", exception)
+            }
        }
network/src/dev/kotlin/team.aliens.dms.android.network/notification/datasource/NetworkNotificationDataSourceImpl.kt-41-42 (1)

41-42: Remove nullable return type from updateNotificationReadStatus in both interface and implementation.

runCatching always returns a Result instance, never null. The nullable return type Result<Unit>? is inconsistent with all other methods in this class and the interface, which correctly use non-nullable Result<T>.

Suggested fix

Interface (NetworkNotificationDataSource.kt, line 33):

-    abstract suspend fun updateNotificationReadStatus(notification: UUID): Result<Unit>?
+    abstract suspend fun updateNotificationReadStatus(notification: UUID): Result<Unit>

Implementation (NetworkNotificationDataSourceImpl.kt, lines 41-42):

-    override suspend fun updateNotificationReadStatus(notification: UUID): Result<Unit>? =
+    override suspend fun updateNotificationReadStatus(notification: UUID): Result<Unit> =
         runCatching { notificationApiService.updateNotificationReadStatus(notification) }
feature/src/dev/kotlin/team/aliens/dms/android/feature/signin/ui/SigninScreen.kt-119-122 (1)

119-122: Redundant navigation bar padding.

systemBarsPadding() already applies padding for both status bar and navigation bar. Adding navigationBarsPadding() after it results in double padding for the navigation bar area.

πŸ”§ Suggested fix
         modifier = Modifier
             .fillMaxSize()
             .imePadding()
             .background(DmsTheme.colorScheme.surfaceTint)
             .systemBarsPadding()
-            .navigationBarsPadding()
             .pointerInput(Unit) { // TODO KMP κ΅¬ν˜„
feature/src/dev/kotlin/team/aliens/dms/android/feature/point/ui/PointHistoryScreen.kt-99-105 (1)

99-105: remember key doesn't capture all dependencies.

The remembered computation accesses state.plusPointList and state.minusPointList, but the key only includes state.allPointList. If those lists update independently, the UI may show stale data.

πŸ› οΈ Suggested fix
-                val pointHistoryList = remember(page, state.allPointList) {
+                val pointHistoryList = remember(page, state.allPointList, state.plusPointList, state.minusPointList) {
                     when (tabData[page]) {
                         PointTab.All -> state.allPointList
                         PointTab.BONUS -> state.plusPointList
                         PointTab.Minus -> state.minusPointList
                     }
                 }

Alternatively, you could remove the remember entirely since accessing lists from state is already cheap:

-                val pointHistoryList = remember(page, state.allPointList, state.plusPointList, state.minusPointList) {
-                    when (tabData[page]) {
-                        PointTab.All -> state.allPointList
-                        PointTab.BONUS -> state.plusPointList
-                        PointTab.Minus -> state.minusPointList
-                    }
-                }
+                val pointHistoryList = when (tabData[page]) {
+                    PointTab.All -> state.allPointList
+                    PointTab.BONUS -> state.plusPointList
+                    PointTab.Minus -> state.minusPointList
+                }
feature/src/dev/kotlin/team/aliens/dms/android/feature/point/ui/PointHistoryScreen.kt-68-71 (1)

68-71: Using pointType.ordinal for initial page is fragile.

The enum order (ALL, BONUS, MINUS) currently matches the tabData list order, but this coupling is implicit and brittle. If either the PointType enum order or the tabData list order changes without the other, the indices will silently mismatch. Use an explicit mapping instead:

         val pagerState = rememberPagerState(
             pageCount = { tabData.size },
-            initialPage = pointType.ordinal,
+            initialPage = tabData.indexOfFirst { it.pointType == pointType }.coerceAtLeast(0),
         )
network/src/dev/kotlin/team.aliens.dms.android.network/notification/datasource/NetworkNotificationDataSource.kt-3-3 (1)

3-3: Unused import: retrofit2.http.PATCH

The @PATCH annotation is a Retrofit HTTP method annotation used in API service interfaces, not in data source classes. This import appears unused in this abstract class and should be removed.

🧹 Proposed fix
-import retrofit2.http.PATCH
feature/src/dev/kotlin/team/aliens/dms/android/feature/main/application/ui/ApplicationScreen.kt-75-77 (1)

75-77: Redundant padding: systemBarsPadding() already includes navigation bar insets.

systemBarsPadding() applies padding for all system bars (status bar + navigation bar). Adding navigationBarsPadding() afterward results in double bottom padding. Remove one of these calls.

Suggested fix
         .background(DmsTheme.colorScheme.background)
-        .systemBarsPadding()
-        .navigationBarsPadding(),
+        .systemBarsPadding(),
feature/src/dev/kotlin/team/aliens/dms/android/feature/notification/viewmodel/NotificationViewModel.kt-54-61 (1)

54-61: Handle null result from updateNotificationReadStatus explicitly.

The method returns Result<Unit>? (nullable). Using ?.fold() means if the result is null, neither success nor failure handlers execute, leaving the operation untracked. Add explicit null handling or verify that null can never be returned and update the return type accordingly.

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

100-100: Inconsistent nullable handling on fileUrl.

Line 96 accesses fileUrl.fileUrl (non-nullable), but line 100 uses fileUrl?.fileUrl.toString() with a safe call. If fileUrl could be null, line 96 would throw NPE. If it can't be null (which seems correct given the onSuccess context), the ?. is misleading and produces the string "null" if the inner fileUrl property is null.

πŸ› Proposed fix
-                        sendEffect(SelectProfileSideEffect.SuccessProfileImage(fileUrl?.fileUrl.toString()))
+                        sendEffect(SelectProfileSideEffect.SuccessProfileImage(fileUrl.fileUrl))
data/src/dev/kotlin/team.aliens.dms.android.data/notification/repository/NotificationRepository.kt-38-39 (1)

38-39: Inconsistent return type: Result<Unit>? vs Result<Unit>.

All other methods in this repository return non-nullable Result<T>, but updateNotificationReadStatus returns Result<Unit>?. This creates an inconsistencyβ€”callers must handle null-checking (?.fold()) even though runCatching always returns a non-null Result<Unit>.

Change to Result<Unit> for consistency:

♻️ Suggested fix
-    abstract suspend fun updateNotificationReadStatus(notification: UUID): Result<Unit>?
+    abstract suspend fun updateNotificationReadStatus(notification: UUID): Result<Unit>
feature/src/dev/kotlin/team/aliens/dms/android/feature/main/mypage/ui/MyPageScreen.kt-70-72 (1)

70-72: Redundant padding modifiers.

systemBarsPadding() already applies insets for both status bar and navigation bar. Adding navigationBarsPadding() after it will cause double padding at the bottom of the screen.

πŸ› Proposed fix
         modifier = Modifier
             .fillMaxSize()
             .background(DmsTheme.colorScheme.background)
-            .systemBarsPadding()
-            .navigationBarsPadding(),
+            .systemBarsPadding(),
feature/src/dev/kotlin/team/aliens/dms/android/feature/notification/ui/NotificationScreen.kt-203-229 (1)

203-229: Modifier parameter incorrectly reused on child elements.

The modifier parameter received by NotificationItem is being applied to child elements (Column, Row, Icon, Text). This is incorrect because the parent's modifier (which may include sizing, padding, or weight constraints from the caller) should not propagate to children. Each child should use Modifier or a dedicated modifier.

The same issue exists in NoticeItem (lines 263-289).

πŸ› Proposed fix for NotificationItem
         Column(
-            modifier = modifier.startPadding(12.dp),
+            modifier = Modifier.startPadding(12.dp),
         ) {
             Text(
                 text = notification.title,
                 style = DmsTheme.typography.bodyM,
             )
             Row(
-                modifier = modifier.topPadding(6.dp)
+                modifier = Modifier.topPadding(6.dp)
             ) {
                 if (!notification.isRead) {
                     Icon(
-                        modifier = modifier.size(4.dp),
+                        modifier = Modifier.size(4.dp),
                         imageVector = Icons.Filled.Circle,
                         contentDescription = null,
                         tint = DmsTheme.colorScheme.primaryContainer,
                     )
                 }
                 Text(
-                    modifier = modifier
+                    modifier = Modifier
                         .startPadding(4.dp),
                     text = notification.content,
                     style = DmsTheme.typography.labelM,
                 )
             }
         }
-        Spacer(modifier = modifier.weight(1f))
+        Spacer(modifier = Modifier.weight(1f))
feature/src/dev/kotlin/team/aliens/dms/android/feature/main/home/ui/HomeScreen.kt-86-88 (1)

86-88: Redundant navigation bar padding causes double inset.

systemBarsPadding() already applies padding for both the status bar and navigation bar. Adding .navigationBarsPadding() afterwards results in the navigation bar inset being applied twice, causing extra unwanted space at the bottom.

πŸ”§ Proposed fix
         modifier = Modifier
             .fillMaxSize()
             .background(DmsTheme.colorScheme.background)
-            .systemBarsPadding()
-            .navigationBarsPadding(),
+            .systemBarsPadding(),
🧹 Nitpick comments (28)
core/design-system/src/dev/java/team/aliens/dms/android/core/designsystem/foundation/DmsIcon.kt (1)

33-33: LGTM!

The icon mappings correctly distinguish between the notification bell icon and the notice megaphone icon, supporting the new notification/notice screens.

Minor nit: The Notice property (line 56) is added at the end of the object rather than in alphabetical order after Notification (line 33). Consider placing it alphabetically for consistency if the team follows that convention.

Also applies to: 56-56

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

118-120: Remove empty onSizeChanged callback.

This callback is invoked on size changes but does nothing. Either implement the intended behavior or remove the dead code.

♻️ Suggested fix
             Box(
                 modifier = Modifier
                     .horizontalPadding(16.dp)
                     .verticalPadding(24.dp)
                     .clip(RoundedCornerShape(32.dp))
-                    .onSizeChanged {
-
-                    }
             ) {
feature/src/dev/kotlin/team/aliens/dms/android/feature/setting/ui/component/SettingRotateContent.kt (1)

40-47: LGTM! Animation changes are well-structured.

The switch from 360Β° to 180Β° rotation is semantically correct for a toggle component, and the explicit tween configuration provides better control over the animation timing.

Minor nitpick: delayMillis = 0 is the default value for tween() and could be omitted for brevity.

Optional: Remove redundant default parameter
     val rotationAngle by animateFloatAsState(
         targetValue = if (rotated) 180f else 0f,
         animationSpec = tween(
             durationMillis = 500,
-            delayMillis = 0,
             easing = LinearEasing
         ),
     )
network/src/dev/kotlin/team.aliens.dms.android.network/notification/apiservice/NotificationApiService.kt (1)

56-59: Nullable Response<Unit>? return type is likely unnecessary.

Retrofit suspend functions throw exceptions on network failures or HTTP errors rather than returning null. The nullable return type here is unusual and inconsistent with the other methods in this interface. This may lead to unnecessary null-checks in the calling code.

Consider changing to Response<Unit> (non-nullable) if you need response metadata, or simply omit the return type entirely (implicit Unit) if you only care about success/failure:

Suggested fix
     `@PATCH`("/notifications/{notification-of-user-id}/read")
     suspend fun updateNotificationReadStatus(
         `@Path`("notification-of-user-id") notificationOfUserId: UUID,
-    ): Response<Unit>?
+    ): Response<Unit>
network/src/dev/kotlin/team.aliens.dms.android.network/user/apiservice/UserApiService.kt (1)

3-3: Unused import after refactoring.

The retrofit2.Response import is no longer used since editPassword now returns Unit directly instead of Response<Unit>?.

🧹 Proposed fix
 package team.aliens.dms.android.network.user.apiservice
 
-import retrofit2.Response
 import retrofit2.http.Body
 import retrofit2.http.GET
 import retrofit2.http.PATCH
network/src/dev/kotlin/team.aliens.dms.android.network/user/datasource/NetworkUserDataSourceImpl.kt (1)

3-3: Unused import remaining from previous error handling.

PasswordMismatchException is imported but no longer used after removing the handleNetworkRequest-based implementation.

🧹 Proposed fix
 package team.aliens.dms.android.network.user.datasource
 import team.aliens.dms.android.network.user.apiservice.UserApiService
-import team.aliens.dms.android.network.user.exception.PasswordMismatchException
 import team.aliens.dms.android.network.user.model.EditPasswordRequest
 import javax.inject.Inject
core/jwt/src/main/java/team/aliens/dms/android/core/jwt/JwtProviderImpl.kt (2)

14-14: Unused import.

The now import is not used anywhere in the visible code. Remove it to keep imports clean.

-import team.aliens.dms.android.shared.date.util.now

3-3: Consider removing Log import if all debug statements are removed.

Once the debug Log.d calls are removed as suggested above, this import will become unused and should also be removed.

feature/src/dev/kotlin/team/aliens/dms/android/feature/signin/ui/SigninScreen.kt (2)

79-85: Redundant SDK version check.

The notificationPermissionGranted helper already returns true for SDK versions below TIRAMISU, so if the condition at line 80 is true, we're guaranteed Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU. The inner check is unnecessary.

♻️ Suggested simplification
     LaunchedEffect(Unit) {
         if (!notificationPermissionGranted(context)) {
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
-                requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
-            }
+            requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
         }
     }

106-106: Unnecessary ExperimentalLayoutApi opt-in.

The imePadding(), systemBarsPadding(), and navigationBarsPadding() APIs used here are stable and don't require this opt-in. Consider removing it unless experimental APIs are intended to be added.

feature/src/dev/kotlin/team/aliens/dms/android/feature/point/viewmodel/PointHistoryViewModel.kt (2)

13-15: Consider making pointRepository private.

The repository is an injected dependency and should not be exposed publicly. This is a common convention for ViewModel dependencies.

♻️ Suggested fix
 internal class PointHistoryViewModel `@Inject` constructor(
-    val pointRepository: PointRepository
+    private val pointRepository: PointRepository
 ) : BaseStateViewModel<PointHistoryState, PointHistorySideEffect>(PointHistoryState()) {

43-45: initTab() and initialTab appear to be dead code.

The screen now uses pointType.ordinal for the initial page (line 70 in PointHistoryScreen.kt), making initTab() and state.initialTab unused. Consider removing this dead code.

♻️ Suggested cleanup
     init {
         getPoints()
-        initTab()
     }
-
-    private fun initTab() {
-        setState { it.copy(initialTab = 0) }
-    }
 }

 internal data class PointHistoryState(
     val allPointList: List<Point> = emptyList(),
     val plusPointList: List<Point> = emptyList(),
     val minusPointList: List<Point> = emptyList(),
-    val initialTab: Int = 0,
 )

Also applies to: 52-52

feature/src/dev/kotlin/team/aliens/dms/android/feature/point/ui/PointHistoryScreen.kt (1)

131-133: Inconsistent naming convention in PointTab sealed class.

BONUS uses SCREAMING_CASE while All and Minus use PascalCase. Consider using a consistent naming convention for all data objects.

♻️ Suggested fix for consistency
 internal sealed class PointTab(
     val title: String,
     val pointType: PointType,
 ) {
-    data object All : PointTab(title = "전체", pointType = PointType.ALL)
-    data object BONUS : PointTab(title = "상점", pointType = PointType.BONUS)
-    data object Minus : PointTab(title = "벌점", pointType = PointType.MINUS)
+    data object All : PointTab(title = "전체", pointType = PointType.ALL)
+    data object Bonus : PointTab(title = "상점", pointType = PointType.BONUS)
+    data object Minus : PointTab(title = "벌점", pointType = PointType.MINUS)
 }

This would require updating references at lines 65 and 102 back to PointTab.Bonus.

network/src/dev/kotlin/team.aliens.dms.android.network/student/apiservice/StudentApiService.kt (1)

3-3: Unused import: retrofit2.Response

After removing the Response<Unit>? return types from resetPassword and editProfile, the Response import appears to be unused in this file.

🧹 Suggested fix
-import retrofit2.Response
network/src/dev/kotlin/team.aliens.dms.android.network/student/datasource/NetworkStudentDataSource.kt (1)

32-32: Minor formatting issue: missing space before Result.

🧹 Suggested fix
-    abstract suspend fun resetPassword(request: ResetPasswordRequest):Result<Unit>
+    abstract suspend fun resetPassword(request: ResetPasswordRequest): Result<Unit>
shared/date/src/main/java/team/aliens/dms/android/shared/date/Extensions.kt (2)

43-58: Consider handling edge case when this is after now.

If this is in the future relative to now, ChronoUnit.*.between() returns negative values, resulting in strings like "-5초 μ „" (Korean: "-5 seconds ago"). Consider adding a guard or documenting the expected invariant.

πŸ’‘ Suggested defensive guard
 fun LocalDateTime.toElapsedText(now: LocalDateTime): String {
     val seconds = ChronoUnit.SECONDS.between(this, now)
+    if (seconds < 0) return "방금 μ „" // or throw, or handle as needed
     if (seconds < 60) return "${seconds}초 μ „"

53-57: Minor inconsistency between day and month thresholds.

Using days < 30 as the cutoff before switching to months may cause edge cases. For example, on day 30 the code switches to ChronoUnit.MONTHS.between(), which uses calendar months (varying 28-31 days). A date 30 days ago might show as "0달 μ „" if it falls within the same calendar month.

Consider using a slightly higher day threshold (e.g., 31) or using months >= 1 as the switch condition for consistency.

data/src/dev/kotlin/team.aliens.dms.android.data/file/repository/FileRepository.kt (1)

3-3: Unused import.

FileUriExposedException does not appear to be used in this abstract class. Consider removing it if it's not needed.

🧹 Suggested fix
 package team.aliens.dms.android.data.file.repository
 
-import android.os.FileUriExposedException
 import team.aliens.dms.android.data.file.model.FileUrl
 import team.aliens.dms.android.data.file.model.PresignedFileUrl
 import java.io.File
feature/src/dev/kotlin/team/aliens/dms/android/feature/notification/viewmodel/NotificationViewModel.kt (1)

79-83: Unused field isRecent in state.

The isRecent field is declared with a default value but is never updated anywhere in the ViewModel. Consider removing it if unused, or implementing the intended functionality.

data/src/dev/kotlin/team.aliens.dms.android.data/student/repository/StudentRepositoryImpl.kt (2)

74-81: Remove commented-out code.

The TODO comment and commented block should be addressed or removed. Leaving commented code in the codebase reduces readability and creates maintenance burden.


98-106: Remove commented-out code.

Same as aboveβ€”this commented block should be removed to keep the codebase clean.

data/src/dev/kotlin/team.aliens.dms.android.data/notification/model/Notification.kt (1)

26-26: PointType.valueOf() may throw on unexpected input from the backend.

The mapping uses PointType.valueOf(this.pointDetailTopic ?: "ALL"). While the null case is handled with the elvis operator, valueOf() will throw IllegalArgumentException if the backend sends a non-null string that doesn't match any enum constant (e.g., a typo, unexpected value, or new enum value added to the backend before the app is updated).

Consider using a safer approach:

Suggested safer mapping
pointDetailTopic = runCatching { PointType.valueOf(this.pointDetailTopic ?: "ALL") }
    .getOrDefault(PointType.ALL),

Or, if PointType uses Kotlin 1.9+ entries:

pointDetailTopic = PointType.entries.find { it.name == this.pointDetailTopic } ?: PointType.ALL,
feature/src/dev/kotlin/team/aliens/dms/android/feature/signin/viewmodel/SignInViewModel.kt (1)

73-79: Fragile error code extraction from exception message.

Parsing the error code via it.message?.drop(5)?.trim() assumes a rigid message format (e.g., "HTTP 400 ..."). If the backend or network layer changes the message format, this will silently fall through to the generic fallback. Consider one of these alternatives:

  1. Define a custom exception type with an explicit errorCode property
  2. Use a sealed class hierarchy for authentication errors
  3. Parse the message with a regex that validates the expected format

The TODO comment indicates awarenessβ€”flagging this for tracking.

♻️ Example: Custom exception with explicit error code
// In a shared/errors package
sealed class AuthException(message: String, val code: Int) : Exception(message) {
    class BadRequest(message: String) : AuthException(message, 400)
    class InvalidCredentials(message: String) : AuthException(message, 422)
    class NotFound(message: String) : AuthException(message, 404)
    class ServerError(message: String) : AuthException(message, 500)
}

// In SignInViewModel
val errorMessage = when ((it as? AuthException)?.code) {
    400 -> "잘λͺ»λœ λΉ„λ°€λ²ˆν˜Έ ν˜•μ‹μ΄μ—μš”"
    422 -> "λΉ„λ°€λ²ˆν˜Έκ°€ μΌμΉ˜ν•˜μ§€ μ•Šμ•„μš”"
    404 -> "μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν•™μƒμž…λ‹ˆλ‹€"
    500 -> "μ„œλ²„μ— λ¬Έμ œκ°€ λ°œμƒν–ˆμ–΄μš”"
    else -> "μΌμ‹œμ μΈ 였λ₯˜λ‘œ λ‘œκ·ΈμΈμ„ ν•  수 μ—†μ–΄μš”"
}
data/src/dev/kotlin/team.aliens.dms.android.data/auth/repository/AuthRepository.kt (1)

5-6: Network layer models leaking into repository interface.

The repository interface imports SignInResponse and CheckIdExistsResponse directly from the network layer (team.aliens.dms.android.network.auth.model). This creates coupling between the data layer and network layer, breaking clean architecture boundaries.

Compare with HashedEmail on line 4, which correctly uses a data layer model. Consider defining data layer equivalents and mapping in the implementation.

♻️ Suggested approach
// In data/auth/model/
data class SignInResult(
    val accessToken: String,
    val refreshToken: String,
    // other relevant fields
)

data class IdExistsResult(
    val schoolId: UUID,
    val email: String,
)

// In AuthRepository.kt
abstract suspend fun signIn(...): Result<SignInResult>
abstract suspend fun checkIdExists(accountId: String): Result<IdExistsResult>

// Mapping happens in AuthRepositoryImpl

Also applies to: 14-14, 27-27

feature/src/dev/kotlin/team/aliens/dms/android/feature/notice/viewmodel/NoticeDetailViewModel.kt (1)

37-55: Naming inconsistency between Notice and Notification.

The ViewModel is named NoticeDetailViewModel, but the state and UI classes use Notification prefix (NotificationDetailUi, NotificationDetailState, NotificationDetailSideEffect). Since this screen is for notices (곡지), consider renaming for consistency:

  • NotificationDetailUi β†’ NoticeDetailUi
  • NotificationDetailState β†’ NoticeDetailState
  • NotificationDetailSideEffect β†’ NoticeDetailSideEffect
feature/src/dev/kotlin/team/aliens/dms/android/feature/notice/ui/NoticeDetailScreen.kt (1)

91-97: Consider using vertical scroll instead of horizontal scroll for text content.

Applying horizontalScroll to a Column containing text content is unusual. Users typically expect to scroll vertically through long text. This will cause the content to scroll horizontally, which may truncate text or create unexpected UX.

♻️ Suggested change
     Column(
         modifier = modifier
             .fillMaxWidth()
             .horizontalPadding(10.dp)
             .background(color = DmsTheme.colorScheme.surfaceTint, shape = RoundedCornerShape(32.dp))
             .padding(vertical = 24.dp, horizontal = 32.dp)
-            .horizontalScroll(rememberScrollState()),
+            .verticalScroll(rememberScrollState()),
     ) {
feature/src/dev/kotlin/team/aliens/dms/android/feature/notification/ui/NotificationScreen.kt (1)

186-302: Consider extracting shared layout logic between NotificationItem and NoticeItem.

Both composables share nearly identical structure with only minor differences (icon, click handler, slight padding). Consider extracting a common base composable to reduce duplication.

data/src/dev/kotlin/team.aliens.dms.android.data/auth/repository/AuthRepositoryImpl.kt (1)

62-68: Remove commented-out code and address TODO.

Lines 64-67 contain commented-out code and a TODO comment. If the usecase implementation is planned, consider creating an issue to track it. Otherwise, remove the dead code to keep the codebase clean.

♻️ Proposed cleanup
     override suspend fun checkIdExists(accountId: String): Result<CheckIdExistsResponse> =
         networkAuthDataSource.checkIdExists(accountId = accountId)
-// TODO :: usecase ν™œμš©
-//        networkAuthDataSource.checkIdExists(
-//            accountId = accountId,
-//        ).email
-

Would you like me to open an issue to track the usecase implementation mentioned in the TODO?

Comment on lines 12 to +24
override suspend fun fetchSchools(): FetchSchoolsResponse =
handleNetworkRequest { schoolApiService.fetchSchools() }
schoolApiService.fetchSchools()

override suspend fun fetchSchoolVerificationQuestion(schoolId: UUID): FetchSchoolVerificationQuestionResponse =
handleNetworkRequest { schoolApiService.fetchSchoolVerificationQuestion(schoolId) }
schoolApiService.fetchSchoolVerificationQuestion(schoolId)

override suspend fun examineSchoolVerificationQuestion(
schoolId: UUID,
answer: String,
) = handleNetworkRequest { schoolApiService.examineSchoolVerificationQuestion(schoolId, answer) }
) = schoolApiService.examineSchoolVerificationQuestion(schoolId, answer)

override suspend fun examineSchoolVerificationCode(code: String): ExamineSchoolVerificationCodeResponse =
handleNetworkRequest { schoolApiService.examineSchoolVerificationCode(code) }
schoolApiService.examineSchoolVerificationCode(code)
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:

# First, let's look at the actual file to confirm the current state
find . -type f -name "NetworkSchoolDataSourceImpl.kt" | head -5

Repository: team-aliens/DMS-Android

Length of output: 279


🏁 Script executed:

# Find repository implementations that use NetworkSchoolDataSource
rg -l "NetworkSchoolDataSource" --type=kotlin | grep -i repository

Repository: team-aliens/DMS-Android

Length of output: 158


🏁 Script executed:

# Search for where NetworkSchoolDataSource is injected/used
rg "NetworkSchoolDataSource" --type=kotlin -B 2 -A 5 | head -100

Repository: team-aliens/DMS-Android

Length of output: 10193


🏁 Script executed:

cat -n data/src/prod/kotlin/team/aliens/dms/android/data/school/repository/SchoolRepositoryImpl.kt

Repository: team-aliens/DMS-Android

Length of output: 1622


🏁 Script executed:

# Search for handleNetworkRequest implementation/definition
rg -B 2 -A 10 "fun handleNetworkRequest|protected fun handleNetworkRequest" --type=kotlin

Repository: team-aliens/DMS-Android

Length of output: 49


🏁 Script executed:

# Check if SchoolRepositoryImpl wraps calls with Result or error handling
rg -B 5 -A 15 "class SchoolRepositoryImpl" --type=kotlin data/

Repository: team-aliens/DMS-Android

Length of output: 2882


🏁 Script executed:

# Find the definition of handleNetworkRequest in the base class
rg -B 5 -A 15 "handleNetworkRequest" --type=kotlin network/src/prod/

Repository: team-aliens/DMS-Android

Length of output: 50379


🏁 Script executed:

# Check if there are any Result-based error handlers elsewhere
rg "Result<" --type=kotlin data/src/prod/kotlin/team/aliens/dms/android/data/school/ | head -20

Repository: team-aliens/DMS-Android

Length of output: 49


🏁 Script executed:

# Find the handleNetworkRequest implementation
find . -name "*.kt" -type f | xargs grep -l "fun handleNetworkRequest" | head -5

Repository: team-aliens/DMS-Android

Length of output: 49


🏁 Script executed:

# Look for the core network util module
fd -t f "handleNetworkRequest" --type=kotlin core/

Repository: team-aliens/DMS-Android

Length of output: 240


🏁 Script executed:

# Find handleNetworkRequest in the core module
find . -path "*/core/network*" -name "*.kt" | xargs grep -l "fun handleNetworkRequest" 2>/dev/null | head -5

Repository: team-aliens/DMS-Android

Length of output: 49


🏁 Script executed:

# Search for handleNetworkRequest definition anywhere
rg "fun handleNetworkRequest" --type=kotlin -B 2 -A 20

Repository: team-aliens/DMS-Android

Length of output: 49


🏁 Script executed:

# Search for handleNetworkRequest in core network util
find . -path "*core*network*util*" -name "*.kt" | head -20

Repository: team-aliens/DMS-Android

Length of output: 606


🏁 Script executed:

# Try a broader search without type restriction
rg "handleNetworkRequest" --type-list | head -20

Repository: team-aliens/DMS-Android

Length of output: 456


🏁 Script executed:

# Search more broadly for where handleNetworkRequest is defined
rg -i "handleNetworkRequest" -A 5 | grep -A 5 "fun handleNetworkRequest" | head -50

Repository: team-aliens/DMS-Android

Length of output: 49


🏁 Script executed:

cat -n ./core/network/src/main/java/team/aliens/dms/android/core/network/util/NetworkRequestHandler.kt

Repository: team-aliens/DMS-Android

Length of output: 2811


🏁 Script executed:

# Also check the dev version to see if it still uses handleNetworkRequest
cat -n ./network/src/dev/kotlin/team.aliens.dms.android.network/school/datasource/NetworkSchoolDataSourceImpl.kt

Repository: team-aliens/DMS-Android

Length of output: 1474


Missing error handling after handleNetworkRequest removal.

The removal of the handleNetworkRequest wrapper from the dev version has eliminated centralized error handling. The handleNetworkRequest function (in core/network/util/NetworkRequestHandler.kt) catches HttpException and other network errors, converting them to custom exceptions. With this wrapper removed, network exceptions will propagate uncaught to callers.

The repository layer (SchoolRepositoryImpl.kt) provides no error handlingβ€”it directly calls the datasource methods and unwraps responses without any Result wrapper or try-catch blocks. This leaves the dev build vulnerable to unhandled network exceptions.

Add error handling at the repository level using Result wrapper or try-catch blocks to safely handle network failures from the datasource.

Comment on lines 16 to +49
override suspend fun fetchStudyRoomApplicationTime(): FetchStudyRoomApplicationTimeResponse =
handleNetworkRequest { studyRoomApiService.fetchStudyRoomApplicationTime() }
studyRoomApiService.fetchStudyRoomApplicationTime()

override suspend fun applySeat(
seatId: UUID,
timeslot: UUID,
) {
handleNetworkRequest { studyRoomApiService.applySeat(seatId, timeslot) }
studyRoomApiService.applySeat(seatId, timeslot)
}

override suspend fun cancelSeat(
seatId: UUID,
timeslot: UUID,
) {
handleNetworkRequest { studyRoomApiService.cancelSeat(seatId, timeslot) }
studyRoomApiService.cancelSeat(seatId, timeslot)
}

override suspend fun fetchStudyRooms(timeslot: UUID): FetchStudyRoomsResponse =
handleNetworkRequest { studyRoomApiService.fetchStudyRooms(timeslot) }
studyRoomApiService.fetchStudyRooms(timeslot)

override suspend fun fetchStudyRoomDetails(
studyRoomId: UUID,
timeslot: UUID,
): FetchStudyRoomDetailsResponse =
handleNetworkRequest { studyRoomApiService.fetchStudyRoomDetails(studyRoomId, timeslot) }
studyRoomApiService.fetchStudyRoomDetails(studyRoomId, timeslot)

override suspend fun fetchAppliedStudyRoom(): FetchAppliedStudyRoomResponse =
handleNetworkRequest { studyRoomApiService.fetchAppliedStudyRoom() }
studyRoomApiService.fetchAppliedStudyRoom()

override suspend fun fetchSeatTypes(studyRoomId: UUID): FetchSeatTypesResponse =
handleNetworkRequest { studyRoomApiService.fetchSeatTypes(studyRoomId) }
studyRoomApiService.fetchSeatTypes(studyRoomId)

override suspend fun fetchAvailableStudyRoomTimes(): FetchAvailableStudyRoomTimesResponse =
handleNetworkRequest { studyRoomApiService.fetchAvailableStudyRoomTimes() }
studyRoomApiService.fetchAvailableStudyRoomTimes()
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:

# Find repository implementations that call these data source methods
rg -n -A 10 "NetworkStudyRoomDataSource|studyRoomDataSource" --type kotlin -g '!**/datasource/**'

Repository: team-aliens/DMS-Android

Length of output: 5355


🏁 Script executed:

cat -n data/src/prod/kotlin/team/aliens/dms/android/data/studyroom/repository/StudyRoomRepositoryImpl.kt

Repository: team-aliens/DMS-Android

Length of output: 2654


🏁 Script executed:

rg -n "StudyRoomRepository\|studyRoomRepository" --type kotlin -g '!**/datasource/**' -g '!**/repository/**' | head -30

Repository: team-aliens/DMS-Android

Length of output: 49


🏁 Script executed:

rg -n "StudyRoomRepository" --type kotlin | grep -v "datasource\|repository/StudyRoomRepository" | head -40

Repository: team-aliens/DMS-Android

Length of output: 1590


🏁 Script executed:

cat -n feature/src/prod/kotlin/team/aliens/dms/android/feature/studyroom/details/StudyRoomDetailsViewModel.kt

Repository: team-aliens/DMS-Android

Length of output: 18803


🏁 Script executed:

cat -n feature/src/prod/kotlin/team/aliens/dms/android/feature/main/application/ApplicationViewModel.kt

Repository: team-aliens/DMS-Android

Length of output: 4805


Address incomplete error handling throughout the data flow.

Removing the handleNetworkRequest wrapper without implementing proper error handling at downstream layers has created a critical gap. The repository methods have no error handling, and ViewModels only implement .onSuccess handlersβ€”network exceptions will either silently fail (read operations) or only log to console (write operations with TODO comments) without informing the UI. Implement .onFailure handlers in all repository call sites to properly handle and report errors to the UI state.

Comment on lines 20 to 24
override suspend fun fetchCreateVotingItem(votingTopicId: UUID, selectedId: UUID): Result<Unit> =
runCatching {
votingApiService.fetchCreateVotingItem(votingTopicId, selectedId)
?.getOrThrow() ?: Unit
}
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

🧩 Analysis chain

🏁 Script executed:

# First, find the VotingApiService file
fd -t f "VotingApiService" --extension=kt

Repository: team-aliens/DMS-Android

Length of output: 253


🏁 Script executed:

# Search for the VotingApiService interface definition
rg -n --type=kotlin "interface VotingApiService" -A 30

Repository: team-aliens/DMS-Android

Length of output: 7958


🏁 Script executed:

# Read the NetworkVotingDataSourceImpl.kt file in full to see all methods
cat -n network/src/dev/kotlin/team.aliens.dms.android.network/voting/datasource/NetworkVotingDataSourceImpl.kt

Repository: team-aliens/DMS-Android

Length of output: 1892


Refactor to avoid double-Result wrapping.

The dev version's votingApiService.fetchCreateVotingItem() returns Result<Unit>?, and wrapping it again in runCatching causes the API's Result to be unwrapped with getOrThrow() and then re-wrapped when exceptions are caughtβ€”a double-wrapping pattern.

Use the flattened approach instead:

override suspend fun fetchCreateVotingItem(votingTopicId: UUID, selectedId: UUID): Result<Unit> =
    votingApiService.fetchCreateVotingItem(votingTopicId, selectedId)
        ?.map { Unit }
        ?: Result.success(Unit)

This aligns with how fetchDeleteVotingItem() handles its optional responseβ€”both are write operations returning Result<Unit> and should use consistent patterns.

πŸ€– Prompt for AI Agents
In
`@network/src/dev/kotlin/team.aliens.dms.android.network/voting/datasource/NetworkVotingDataSourceImpl.kt`
around lines 20 - 24, The current implementation of
NetworkVotingDataSourceImpl.fetchCreateVotingItem double-wraps the API result by
calling votingApiService.fetchCreateVotingItem(...) inside runCatching and then
getOrThrow(), which rewraps exceptions; instead, return the API Result directly
and flatten nulls like fetchDeleteVotingItem does β€” call
votingApiService.fetchCreateVotingItem(votingTopicId, selectedId)?.map { Unit }
?: Result.success(Unit) so you don't re-wrap errors and maintain consistency
with fetchDeleteVotingItem.

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

Caution

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

⚠️ Outside diff range comments (4)
feature/src/dev/kotlin/team/aliens/dms/android/feature/vote/ui/VoteScreen.kt (1)

40-51: Potential premature error snackbar on initial composition.

snapshotFlow emits the current value immediately on collection. If resultStore.resultStateMap["vote_result"] is not yet set when the screen opens, result will be null and the error snackbar will display immediatelyβ€”even though no actual failure occurred.

Consider filtering out the initial null or tracking whether a result was expected:

Suggested fix: filter initial null emission
     LaunchedEffect(Unit) {
         snapshotFlow {
             resultStore.resultStateMap["vote_result"]?.value as? AllVoteSearch?
-        }.collect { result ->
+        }.filterNotNull()
+         .collect { result ->
-            if (result != null) {
                 viewModel.initState(result)
                 resultStore.removeResult<String?>(resultKey = "vote_result")
-            } else {
-                onShowSnackBar(DmsSnackBarType.ERROR, "정보λ₯Ό κ°€μ Έμ˜€μ§€ λͺ» ν–ˆμ–΄μš”")
-            }
         }
     }

If you need to handle the case where navigation occurs without a result, consider checking this separately (e.g., via a timeout or an explicit "no result" sentinel value).

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

22-30: Unhandled failure crashes the app.

Throwing the exception on failure (line 27) will crash the application when fetchMyPage() fails (e.g., network error). This is inconsistent with the Result-based error handling pattern used elsewhere in this PR.

πŸ› Proposed fix
     private fun getMyPage() {
         viewModelScope.launch(Dispatchers.IO) {
             studentRepository.fetchMyPage().onSuccess { myPage ->
                 setState { it.copy(myPage = myPage) }
             }.onFailure {
-                throw it
+                // Handle error gracefully - e.g., log or emit a side effect
             }
         }
     }
feature/src/dev/kotlin/team/aliens/dms/android/feature/setting/viewmodel/SettingViewModel.kt (2)

25-36: Missing failure handling for device token loading.

If loadDeviceToken() fails, the failure is silently ignored and fetchNotificationStatus() is never called. Consider handling the failure to provide user feedback or retry logic.

πŸ› Proposed fix
     private fun fetchDeviceToken() {
         viewModelScope.launch {
             deviceDataStoreDataSource.loadDeviceToken().onSuccess { deviceToken ->
                 setState { settingState ->
                     settingState.copy(
                         deviceToken = deviceToken,
                     )
                 }
                 fetchNotificationStatus()
+            }.onFailure {
+                // Consider logging or emitting a side effect for user feedback
             }
         }
     }

38-44: Missing failure handling for sign out.

If signOut() fails, the failure is silently ignored. The user may believe they signed out when they didn't.

πŸ› Proposed fix
     internal fun signOut() {
         viewModelScope.launch(Dispatchers.IO) {
             authRepository.signOut().onSuccess {
                 sendEffect(SettingSideEffect.SignOutSuccess)
+            }.onFailure {
+                // TODO: Send error side effect (e.g., SignOutFailed)
             }
         }
     }
πŸ€– Fix all issues with AI agents
In `@app/src/dev/kotlin/team/aliens/dms/android/app/DmsApp.kt`:
- Around line 326-329: The result key used when navigating is mismatched:
resultStore.setResult<UUID?> is setting "notice_result" but NoticeDetailScreen
expects "notice_detail_result"; update the setter in the
onNavigateNotificationDetailClick handler (the call to resultStore.setResult in
DmsApp.kt where NoticeDetailScreenNav is pushed) to use the exact key
"notice_detail_result" (or alternatively change NoticeDetailScreen to read
"notice_result") so both sides use the same unique key name.

In
`@core/device/src/main/java/team/aliens/dms/android/core/device/datastore/DeviceDataStoreDataSource.kt`:
- Line 5: Change the abstract function loadDeviceToken to be a suspend function
to match storeDeviceToken and clearDeviceToken; update the override in
DeviceDataStoreDataSourceImpl to be suspend and wrap the call to
deviceStore.loadDeviceToken() with suspendRunCatching so it is invoked from a
suspend context (keep the existing deviceStore.loadDeviceToken() call but ensure
it is called inside suspendRunCatching within the suspend override).

In
`@core/device/src/main/java/team/aliens/dms/android/core/device/datastore/DeviceDataStoreDataSourceImpl.kt`:
- Around line 10-18: Prod variant of NotificationRepositoryImpl must be updated
to match the new Result-returning data source methods: change getDeviceToken()
to accept/return the Result<String> from deviceDataStore.loadDeviceToken() (or
propagate it) instead of expecting a raw String, and change saveDeviceToken() to
accept/return the Result<Unit> from deviceDataStore.storeDeviceToken() (or
propagate/handle errors) instead of assigning a Unit; update calls to
deviceDataStore.loadDeviceToken() and deviceDataStore.storeDeviceToken() in
NotificationRepositoryImpl (prod) to work with Result types (e.g., propagate
Result, map/throw as appropriate) so signatures and behavior match the dev
variant and the updated interface.

In `@core/jwt/src/main/java/team/aliens/dms/android/core/jwt/JwtProviderImpl.kt`:
- Around line 44-46: Remove the development debug log before throwing in
JwtProviderImpl: delete the Log.d("TEST _cachedRefreshToken!!.isExpired()", ...)
call that precedes the throw in the branch that checks
_cachedRefreshToken!!.isExpired(); after removing it, ensure the
android.util.Log import is removed if it is no longer referenced anywhere in
JwtProviderImpl.kt so there are no unused imports.
- Around line 80-84: JwtProviderImpl currently launches
CoroutineScope(Dispatchers.Default).launch and calls
jwtDataStoreDataSource.storeTokens inside runCatching with an empty onFailure,
which silences persistence errors; change this to handle failures by either
logging the exception via your logger (e.g., processLogger or Android Log) or by
propagating the error to the caller (e.g., emit a failure result) so token
persistence failures are visible. Locate the
CoroutineScope(Dispatchers.Default).launch block in JwtProviderImpl and replace
the empty onFailure { } with a concrete handler that logs the throwable
(including message and stack) and/or notifies the calling API (returning a
Result or throwing) when jwtDataStoreDataSource.storeTokens fails.

In
`@data/src/dev/kotlin/team.aliens.dms.android.data/meal/repository/MealRepositoryImpl.kt`:
- Around line 17-21: The recoverCatching in fetchMeal masks all updateMeal
failures by converting them to CannotSaveMealException; change it so it
preserves CannotFindMealException and only maps other failures to
CannotSaveMealException. Locate fetchMeal (the runCatching {
databaseMealDataSource.queryMeal(date).toModel() } block) and in the
recoverCatching handler inspect the Throwable/result from updateMeal(date) β€” if
it's a CannotFindMealException rethrow it, otherwise wrap/throw
CannotSaveMealException; ensure you use updateMeal(...).getOrThrow()/fold or
check the exception type instead of unconditionally using getOrElse { throw
CannotSaveMealException() }.

In
`@feature/src/dev/kotlin/team/aliens/dms/android/feature/profile/viewmodel/SelectProfileViewModel.kt`:
- Around line 105-106: The hardcoded Korean message in the onFailure lambda of
SelectProfileViewModel misrepresents different failure causes; update the
onFailure handler that calls
sendEffect(SelectProfileSideEffect.FailFetchPresignedUrl(...)) to pass a
meaningful message from the Throwable (or via an error-mapper utility) instead
of the fixed "이미지가 μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€" so network/permission/server errors are surfaced
(e.g., use throwable.message ?: defaultMessage or
mapThrowableToUserMessage(throwable) and forward that to FailFetchPresignedUrl).
- Around line 93-101: The success branch uses unnecessary safe-call and toString
on a guaranteed non-null value; inside the onSuccess lambda, replace the
sendEffect call to pass the non-null FileUrl.fileUrl directly (matching the
setState update) by using fileUrl.fileUrl when creating
SelectProfileSideEffect.SuccessProfileImage in SelectProfileViewModel’s
onSuccess handler.

In
`@feature/src/dev/kotlin/team/aliens/dms/android/feature/remain/ui/RemainApplicationScreen.kt`:
- Around line 63-68: The Column was changed to fillMaxWidth(), removing vertical
constraints so Spacer(modifier = Modifier.weight(1f)) cannot work; restore a
finite height on the Column (e.g., use fillMaxSize() or add
Modifier.fillMaxHeight()) so weight-based Spacer pushes content to the bottom.
Update the modifier on the Column in RemainApplicationScreen.kt (the Column with
Modifier.fillMaxWidth()) to include a height constraint (fillMaxSize() or
fillMaxHeight()) so Modifier.weight(1f) on the Spacer behaves as intended.

In
`@feature/src/dev/kotlin/team/aliens/dms/android/feature/vote/viewmodel/VoteViewModel.kt`:
- Line 3: Remove the leftover debug logging by deleting the android.util.Log
import and any Log.d("TEST", ...) calls in VoteViewModel (e.g., the Log.d usage
around line 98 in VoteViewModel.kt); ensure you remove both the import statement
and all occurrences of Log.d so there are no unused imports or debug prints left
in the class.
- Around line 99-102: The when-block in VoteViewModel that inspects
it.message?.drop(5)?.trim() to detect "409" is brittle; replace this
string-slice parsing with a robust check: detect the actual exception type or
structured status code (e.g., check if the throwable is an HttpException or a
ConflictException and read its status/code property) and then call
sendEffect(VoteSideEffect.VoteConflict) or VoteFail accordingly; update the
error-handling branch that currently contains the when(...) so it uses the
exception class or explicit status code field instead of message manipulation.

In
`@network/src/dev/kotlin/team.aliens.dms.android.network/user/datasource/NetworkUserDataSourceImpl.kt`:
- Line 3: Remove the unused import of PasswordMismatchException from
NetworkUserDataSourceImpl (it is not referenced after migrating to
suspendRunCatching); open the NetworkUserDataSourceImpl.kt file, delete the line
importing
team.aliens.dms.android.network.user.exception.PasswordMismatchException, run a
Kotlin/IDE quick cleanup or recompile to ensure no other references remain, and
commit the change.
♻️ Duplicate comments (2)
core/jwt/src/main/java/team/aliens/dms/android/core/jwt/JwtProviderImpl.kt (1)

70-72: Silent exception swallowing in loadTokens remains unaddressed.

The empty onFailure block silently swallows exceptions when token loading fails, making debugging difficult. At minimum, consider logging the failure (without sensitive token data).

Proposed fix
-        }.onFailure {
-
+        }.onFailure { exception ->
+            Log.w("JwtProvider", "Failed to load tokens", exception)
         }
feature/src/dev/kotlin/team/aliens/dms/android/feature/notice/ui/NoticeDetailScreen.kt (1)

47-58: Two issues remain: type mismatch and spurious error snackbar on initial composition.

  1. Line 53 uses removeResult<String?> but should be removeResult<UUID?> to match the type being stored and read.

  2. snapshotFlow emits null immediately on first composition (before any result is set), triggering the error snackbar on every screen open. Consider filtering out the initial null emission.

πŸ› Proposed fix
     LaunchedEffect(Unit) {
         snapshotFlow {
             resultStore.resultStateMap["notice_detail_result"]?.value as? UUID?
-        }.collect { result ->
+        }.filterNotNull().collect { result ->
-            if (result != null) {
-                viewModel.getNotificationDetail(result)
-                resultStore.removeResult<String?>(resultKey = "notice_detail_result")
-            } else {
-                onShowSnackBar(DmsSnackBarType.ERROR, "정보λ₯Ό κ°€μ Έμ˜€μ§€ λͺ» ν–ˆμ–΄μš”")
-            }
+            viewModel.getNotificationDetail(result)
+            resultStore.removeResult<UUID?>(resultKey = "notice_detail_result")
         }
     }
🧹 Nitpick comments (10)
feature/src/dev/kotlin/team/aliens/dms/android/feature/remain/ui/RemainApplicationScreen.kt (1)

5-5: Unused import: Box is imported but not used.

This import appears to be dead code. Consider removing it to keep imports clean.

🧹 Suggested fix
-import androidx.compose.foundation.layout.Box
feature/src/dev/kotlin/team/aliens/dms/android/feature/vote/viewmodel/VoteViewModel.kt (1)

87-89: Unused with receiver scope.

The with(uiState.value) block is declared but not utilizedβ€”code inside still references uiState.value explicitly. Either remove the with wrapper or use the receiver scope.

Proposed fix: remove unused with
     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 {
+        viewModelScope.launch(Dispatchers.IO) {
+            setState { uiState.value.copy(isLoading = true, buttonEnabled = false) }
+            voteRepository.fetchCreateVotingItem(
+                votingTopicId = uiState.value.vote.id,
+                selectedId = uiState.value.selectId!!,
+            ).onSuccess {
                 // ... rest unchanged
-                }
             }
         }
     }
core/jwt/src/main/java/team/aliens/dms/android/core/jwt/JwtProviderImpl.kt (1)

14-14: Unused import.

The now import from team.aliens.dms.android.shared.date.util is not used anywhere in this file.

Proposed fix
-import team.aliens.dms.android.shared.date.util.now
shared/exception/src/main/java/team/aliens/dms/android/shared/exception/util/SuspendRunCatching.kt (1)

5-11: Good implementation addressing CancellationException handling.

This correctly rethrows CancellationException to preserve structured concurrency, which addresses the issue flagged in previous reviews about runCatching swallowing cancellation signals.

One minor observation: the function is named suspendRunCatching but accepts a non-suspend lambda () -> T. This works because callers invoke suspend API methods that return values synchronously within the lambda, but the naming could be slightly misleading. Consider either:

  • Renaming to runCatchingCancellable or similar, or
  • Changing the signature to suspend () -> T if you want the name to match the behavior

This is a minor naming nitβ€”the current implementation is functionally correct.

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

14-16: Consider making studentRepository private.

The repository is injected but exposed as a public val. Since it's only used internally, consider making it private.

♻️ Proposed fix
 `@HiltViewModel`
 internal class HomeViewModel `@Inject` constructor(
-    val studentRepository: StudentRepository,
+    private val studentRepository: StudentRepository,
 ) : BaseStateViewModel<HomeState, HomeSideEffect>(HomeState()) {
feature/src/dev/kotlin/team/aliens/dms/android/feature/setting/viewmodel/SettingViewModel.kt (1)

15-19: Consider making injected dependencies private.

The repositories and data source are exposed as public val but appear to only be used internally.

♻️ 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()) {
feature/src/dev/kotlin/team/aliens/dms/android/feature/notice/viewmodel/NoticeDetailViewModel.kt (2)

55-57: Consider including error details in the failure side effect.

FailNoticeDetail is a singleton object that doesn't carry error information. Including the error message would help the UI display meaningful feedback to the user.

♻️ Proposed enhancement
 internal sealed interface NoticeDetailSideEffect {
-    object FailNoticeDetail : NoticeDetailSideEffect
+    data class FailNoticeDetail(val message: String? = null) : NoticeDetailSideEffect
 }

Then update the emission:

-            sendEffect(NoticeDetailSideEffect.FailNoticeDetail)
+            sendEffect(NoticeDetailSideEffect.FailNoticeDetail(it.message))

45-53: Default state uses UUID.randomUUID() as placeholder.

Using UUID.randomUUID() in the default state creates a new random ID each time. This is acceptable for a placeholder, but consider using a constant sentinel value or making notice nullable to clearly distinguish "not loaded" from "loaded" states.

network/src/dev/kotlin/team.aliens.dms.android.network/student/datasource/NetworkStudentDataSourceImpl.kt (1)

79-79: Add explicit return type to withdraw() for consistency.

All other methods explicitly declare their Result<T> return type, but withdraw() relies on type inference. Adding the explicit type improves readability and consistency.

♻️ Suggested change
-    override suspend fun withdraw() = suspendRunCatching { studentApiService.withdraw() }
+    override suspend fun withdraw(): Result<Unit> = suspendRunCatching { studentApiService.withdraw() }
feature/src/dev/kotlin/team/aliens/dms/android/feature/notification/ui/NotificationScreen.kt (1)

202-258: Optional: extract a shared row composable to reduce duplication.

NotificationItem and NoticeItem share nearly identical layout/behavior; a small shared composable would make future UI tweaks safer and cheaper.

Also applies to: 261-318

Comment on lines 10 to 18
override fun loadDeviceToken() = suspendRunCatching { deviceStore.loadDeviceToken() }

override suspend fun storeDeviceToken(deviceToken: String) {
override suspend fun storeDeviceToken(deviceToken: String) = suspendRunCatching {
deviceStore.storeDeviceToken(deviceToken)
}

override suspend fun clearDeviceToken() {
override suspend fun clearDeviceToken() = suspendRunCatching {
deviceStore.clearDeviceToken()
}
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 the DeviceDataStoreDataSource interface
echo "=== Looking for DeviceDataStoreDataSource interface ==="
find . -name "DeviceDataStoreDataSource.kt" -o -name "DeviceDataStoreDataSource.java" | head -20

Repository: team-aliens/DMS-Android

Length of output: 225


🏁 Script executed:

#!/bin/bash
# Search in the device datastore directory for the interface
echo "=== Listing files in device datastore directory ==="
fd -e kt -e java "datastore" --type f | grep -i "DeviceDataStore" | head -30

Repository: team-aliens/DMS-Android

Length of output: 324


🏁 Script executed:

#!/bin/bash
# Get more context about the implementation file structure
echo "=== Checking the directory structure around DeviceDataStoreDataSourceImpl.kt ==="
fd -e kt "DeviceDataStore" | head -30

Repository: team-aliens/DMS-Android

Length of output: 353


🏁 Script executed:

#!/bin/bash
# Examine the interface definition
echo "=== DeviceDataStoreDataSource interface ==="
cat "./core/device/src/main/java/team/aliens/dms/android/core/device/datastore/DeviceDataStoreDataSource.kt"

Repository: team-aliens/DMS-Android

Length of output: 399


🏁 Script executed:

#!/bin/bash
# Examine the implementation again to compare with interface
echo "=== DeviceDataStoreDataSourceImpl implementation ==="
cat "./core/device/src/main/java/team/aliens/dms/android/core/device/datastore/DeviceDataStoreDataSourceImpl.kt"

Repository: team-aliens/DMS-Android

Length of output: 835


🏁 Script executed:

#!/bin/bash
# Search for all usages of these three methods across the codebase
echo "=== Searching for calls to loadDeviceToken, storeDeviceToken, clearDeviceToken ==="
rg -n "\.loadDeviceToken\(\)|\.storeDeviceToken\(|\.clearDeviceToken\(" --type kotlin -B 3 -A 3

Repository: team-aliens/DMS-Android

Length of output: 5876


Critical: Prod variant of NotificationRepositoryImpl is incompatible with these changes.

The implementation correctly matches the updated interface signature (loadDeviceToken(): Result<String>, storeDeviceToken(): Result<Unit>, clearDeviceToken(): Result<Unit>). However, the prod variant of NotificationRepositoryImpl.kt has not been updated and will fail to compile:

  • Line 67: getDeviceToken() expects to return String but loadDeviceToken() now returns Result<String>
  • Line 63: saveDeviceToken() expects to assign Unit but storeDeviceToken() now returns Result<Unit>

The dev variant correctly handles the Result types. Align the prod variant signatures and implementations with the dev variant.

Note: loadDeviceToken() is correctly a non-suspend function matching the interface contract, so suspendRunCatching is appropriate for exception-to-Result conversion here.

πŸ€– Prompt for AI Agents
In
`@core/device/src/main/java/team/aliens/dms/android/core/device/datastore/DeviceDataStoreDataSourceImpl.kt`
around lines 10 - 18, Prod variant of NotificationRepositoryImpl must be updated
to match the new Result-returning data source methods: change getDeviceToken()
to accept/return the Result<String> from deviceDataStore.loadDeviceToken() (or
propagate it) instead of expecting a raw String, and change saveDeviceToken() to
accept/return the Result<Unit> from deviceDataStore.storeDeviceToken() (or
propagate/handle errors) instead of assigning a Unit; update calls to
deviceDataStore.loadDeviceToken() and deviceDataStore.storeDeviceToken() in
NotificationRepositoryImpl (prod) to work with Result types (e.g., propagate
Result, map/throw as appropriate) so signatures and behavior match the dev
variant and the updated interface.

Comment on lines 105 to 106
onFailure = {
sendEffect(SelectProfileSideEffect.FailFetchPresignedUrl("이미지가 μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€"))
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

Failure message is potentially misleading.

Hardcoding β€œμ΄λ―Έμ§€κ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€β€ for all presigned URL failures can misrepresent network/permission/server errors. Consider using the Throwable (or an error mapper) to pick a more accurate message.

πŸ’‘ Minimal adjustment to surface the actual failure
-                onFailure = {
-                    sendEffect(SelectProfileSideEffect.FailFetchPresignedUrl("이미지가 μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€"))
+                onFailure = { throwable ->
+                    sendEffect(
+                        SelectProfileSideEffect.FailFetchPresignedUrl(
+                            throwable.message ?: "ν”„λ‘œν•„ 이미지 μ—…λ‘œλ“œμ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€"
+                        )
+                    )
                 }
πŸ“ 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
onFailure = {
sendEffect(SelectProfileSideEffect.FailFetchPresignedUrl("이미지가 μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€"))
onFailure = { throwable ->
sendEffect(
SelectProfileSideEffect.FailFetchPresignedUrl(
throwable.message ?: "ν”„λ‘œν•„ 이미지 μ—…λ‘œλ“œμ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€"
)
)
}
πŸ€– Prompt for AI Agents
In
`@feature/src/dev/kotlin/team/aliens/dms/android/feature/profile/viewmodel/SelectProfileViewModel.kt`
around lines 105 - 106, The hardcoded Korean message in the onFailure lambda of
SelectProfileViewModel misrepresents different failure causes; update the
onFailure handler that calls
sendEffect(SelectProfileSideEffect.FailFetchPresignedUrl(...)) to pass a
meaningful message from the Throwable (or via an error-mapper utility) instead
of the fixed "이미지가 μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€" so network/permission/server errors are surfaced
(e.g., use throwable.message ?: defaultMessage or
mapThrowableToUserMessage(throwable) and forward that to FailFetchPresignedUrl).

Comment on lines 63 to 68
Column(
modifier = Modifier
.fillMaxSize()
.background(DmsTheme.colorScheme.background),
.fillMaxWidth()
.background(DmsTheme.colorScheme.background)
.systemBarsPadding(),
) {
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

Potential layout issue: weight() modifier requires finite height constraints.

Changing from fillMaxSize() to fillMaxWidth() removes the explicit height constraint on the Column. The Spacer(modifier = Modifier.weight(1f)) at line 109 relies on finite height bounds to push the button to the bottom. Without fillMaxSize() (or at least a height constraint), the weight modifier will have no effect, and the button may not position correctly at the bottom of the screen.

πŸ”§ Suggested fix
     Column(
         modifier = Modifier
-            .fillMaxWidth()
+            .fillMaxSize()
             .background(DmsTheme.colorScheme.background)
             .systemBarsPadding(),
     ) {
πŸ“ 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()
.background(DmsTheme.colorScheme.background),
.fillMaxWidth()
.background(DmsTheme.colorScheme.background)
.systemBarsPadding(),
) {
Column(
modifier = Modifier
.fillMaxSize()
.background(DmsTheme.colorScheme.background)
.systemBarsPadding(),
) {
πŸ€– Prompt for AI Agents
In
`@feature/src/dev/kotlin/team/aliens/dms/android/feature/remain/ui/RemainApplicationScreen.kt`
around lines 63 - 68, The Column was changed to fillMaxWidth(), removing
vertical constraints so Spacer(modifier = Modifier.weight(1f)) cannot work;
restore a finite height on the Column (e.g., use fillMaxSize() or add
Modifier.fillMaxHeight()) so weight-based Spacer pushes content to the bottom.
Update the modifier on the Column in RemainApplicationScreen.kt (the Column with
Modifier.fillMaxWidth()) to include a height constraint (fillMaxSize() or
fillMaxHeight()) so Modifier.weight(1f) on the Spacer behaves as intended.

@@ -1,5 +1,6 @@
package team.aliens.dms.android.feature.vote.viewmodel

import android.util.Log
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

Remove debug logging before merge.

Log.d("TEST", ...) appears to be leftover debug code. Remove it along with the unused android.util.Log import.

Proposed fix
-import android.util.Log
                 }.onFailure {
                     setState { uiState.value.copy(isLoading = false, buttonEnabled = true) }
-                    Log.d("TEST", it.toString())
                     when (it.message?.drop(5)?.trim()) {

Also applies to: 98-98

πŸ€– Prompt for AI Agents
In
`@feature/src/dev/kotlin/team/aliens/dms/android/feature/vote/viewmodel/VoteViewModel.kt`
at line 3, Remove the leftover debug logging by deleting the android.util.Log
import and any Log.d("TEST", ...) calls in VoteViewModel (e.g., the Log.d usage
around line 98 in VoteViewModel.kt); ensure you remove both the import statement
and all occurrences of Log.d so there are no unused imports or debug prints left
in the class.

Comment on lines +99 to 102
when (it.message?.drop(5)?.trim()) {
"409" -> sendEffect(VoteSideEffect.VoteConflict)
else -> sendEffect(VoteSideEffect.VoteFail)
}
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

Fragile error code parsing from exception message.

Parsing the HTTP status code via it.message?.drop(5)?.trim() assumes a very specific message format (e.g., "code 409"). This is brittle and will silently fail if the message format changes or differs across error sources.

Consider either:

  1. Restoring type-based checking (e.g., is ConflictException)
  2. Using a structured error type that exposes the status code explicitly
  3. At minimum, using a regex or more robust parsing
Suggested approach using exception type
                 }.onFailure {
                     setState { uiState.value.copy(isLoading = false, buttonEnabled = true) }
-                    Log.d("TEST", it.toString())
-                    when (it.message?.drop(5)?.trim()) {
-                        "409" -> sendEffect(VoteSideEffect.VoteConflict)
+                    when (it) {
+                        is ConflictException -> sendEffect(VoteSideEffect.VoteConflict)
                         else -> sendEffect(VoteSideEffect.VoteFail)
                     }
                 }
πŸ€– Prompt for AI Agents
In
`@feature/src/dev/kotlin/team/aliens/dms/android/feature/vote/viewmodel/VoteViewModel.kt`
around lines 99 - 102, The when-block in VoteViewModel that inspects
it.message?.drop(5)?.trim() to detect "409" is brittle; replace this
string-slice parsing with a robust check: detect the actual exception type or
structured status code (e.g., check if the throwable is an HttpException or a
ConflictException and read its status/code property) and then call
sendEffect(VoteSideEffect.VoteConflict) or VoteFail accordingly; update the
error-handling branch that currently contains the when(...) so it uses the
exception class or explicit status code field instead of message manipulation.

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

Caution

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

⚠️ Outside diff range comments (1)
app/src/dev/kotlin/team/aliens/dms/android/app/DmsApp.kt (1)

119-130: LaunchedEffect missing isJwtAvailable dependency.

The effect uses isJwtAvailable in its logic (line 122) but only depends on isOnboardingCompleted. If the JWT state changes after onboarding is complete (e.g., token expiration or external logout), the navigation won't update to reflect the new state.

πŸ› Proposed fix
-    LaunchedEffect(isOnboardingCompleted) {
+    LaunchedEffect(isOnboardingCompleted, isJwtAvailable) {
         val initialScreen = when {
             !isOnboardingCompleted -> OnboardingScreenNav
             isJwtAvailable -> HomeScreenNav
             else -> SignInScreenNav
         }
πŸ€– Fix all issues with AI agents
In
`@data/src/dev/kotlin/team.aliens.dms.android.data/meal/repository/MealRepositoryImpl.kt`:
- Around line 19-26: The recoverCatching block currently catches Throwable and
thus swallows coroutine cancellations; update the exception handling in the
block that calls updateMeal(date).getOrThrow() to add a specific catch for
kotlinx.coroutines.CancellationException (re-throw it immediately) before the
generic catch (_: Throwable) that wraps failures into CannotSaveMealException,
and add the corresponding import for CancellationException; keep the existing
catches for CannotFindMealException and CannotSaveMealException handling as-is.

In
`@network/src/dev/kotlin/team.aliens.dms.android.network/user/datasource/NetworkUserDataSourceImpl.kt`:
- Around line 16-19: The dev implementation of comparePassword in
NetworkUserDataSourceImpl uses suspendRunCatching which preserves a 401 as
HttpException instead of mapping it to PasswordMismatchException; change the
implementation of comparePassword to recover 401 responses and throw
PasswordMismatchException (e.g. catch HttpException with statusCode 401 or use
Result.recover/exception mapping) so that comparePassword(password: String)
returns Result.failure(PasswordMismatchException()) for 401 while preserving
other errors unchanged; reference the comparePassword method, suspendRunCatching
call, and PasswordMismatchException when making the change.
♻️ Duplicate comments (1)
feature/src/dev/kotlin/team/aliens/dms/android/feature/profile/viewmodel/SelectProfileViewModel.kt (1)

105-106: Hardcoded failure message still doesn't surface the actual error.

The previous review suggested using the Throwable to provide a more accurate error message. While the message text was improved, the onFailure lambda still doesn't capture the throwable to provide context about what actually failed (network error, permission error, server error, etc.).

πŸ› Proposed fix to capture and use the throwable
-                onFailure = {
-                    sendEffect(SelectProfileSideEffect.FailFetchPresignedUrl("ν”„λ‘œν•„ 이미지 μ—…λ‘œλ“œμ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€"))
+                onFailure = { throwable ->
+                    sendEffect(
+                        SelectProfileSideEffect.FailFetchPresignedUrl(
+                            throwable.message ?: "ν”„λ‘œν•„ 이미지 μ—…λ‘œλ“œμ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€"
+                        )
+                    )
                 }
🧹 Nitpick comments (1)
app/src/dev/kotlin/team/aliens/dms/android/app/DmsApp.kt (1)

143-152: Clarify the back-stack management logic with a comment.

The when block only has one case for HomeScreenNav, and the subsequent removeAll always executes. While the logic works correctly for bottom navigation behavior (ensuring single instances of bottom nav destinations), a brief comment would improve readability.

✏️ Optional: Add clarifying comment
                             if (currentScreen != destination) {
+                                // Remove the destination if already in stack to avoid duplicates
                                 when (destination) {
                                     is HomeScreenNav -> {
                                         backStack.remove(HomeScreenNav)
                                     }
                                 }
+                                // Clear other bottom nav destinations to maintain single-screen-per-tab behavior
                                 backStack.removeAll {
                                     it is ApplicationScreenNav ||
                                     it is MyPageScreenNav
                                 }

Comment on lines +16 to +19
override suspend fun comparePassword(password: String): Result<Unit> =
suspendRunCatching {
userApiService.comparePassword(password)
}
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the file in question
find . -name "NetworkUserDataSourceImpl.kt" -type f

Repository: team-aliens/DMS-Android

Length of output: 271


🏁 Script executed:

#!/bin/bash
# Search for handleNetworkRequest to understand previous implementation
rg "handleNetworkRequest" --type kt -A 5

Repository: team-aliens/DMS-Android

Length of output: 93


🏁 Script executed:

#!/bin/bash
# Search for PasswordMismatchException
rg "PasswordMismatchException" --type kt

Repository: team-aliens/DMS-Android

Length of output: 93


🏁 Script executed:

#!/bin/bash
# Look at git history for changes to comparePassword method
git log --oneline --all -- "*/NetworkUserDataSourceImpl.kt" | head -20

Repository: team-aliens/DMS-Android

Length of output: 328


🏁 Script executed:

#!/bin/bash
# Read the NetworkUserDataSourceImpl.kt file
cat -n "./network/src/dev/kotlin/team.aliens.dms.android.network/user/datasource/NetworkUserDataSourceImpl.kt"

Repository: team-aliens/DMS-Android

Length of output: 985


🏁 Script executed:

#!/bin/bash
# Search for handleNetworkRequest in kotlin files
rg "handleNetworkRequest" --glob "*.kt" -A 5

Repository: team-aliens/DMS-Android

Length of output: 38093


🏁 Script executed:

#!/bin/bash
# Search for PasswordMismatchException
rg "PasswordMismatchException" --glob "*.kt"

Repository: team-aliens/DMS-Android

Length of output: 2060


🏁 Script executed:

#!/bin/bash
# Look at the git diff for the most recent user datasource change
git show 759e5db79 -- "*/NetworkUserDataSourceImpl.kt" | head -100

Repository: team-aliens/DMS-Android

Length of output: 1346


🏁 Script executed:

#!/bin/bash
# Find suspendRunCatching implementation
rg "fun suspendRunCatching" --glob "*.kt" -A 10

Repository: team-aliens/DMS-Android

Length of output: 49


🏁 Script executed:

#!/bin/bash
# Find the full handleNetworkRequest implementation
cat -n core/network/src/main/java/team/aliens/dms/android/core/network/util/NetworkRequestHandler.kt | head -50

Repository: team-aliens/DMS-Android

Length of output: 2811


🏁 Script executed:

#!/bin/bash
# Check if there are recent changes to the dev user datasource
git log --oneline --all -- "network/src/dev/kotlin/team.aliens.dms.android.network/user/datasource/NetworkUserDataSourceImpl.kt" | head -20

Repository: team-aliens/DMS-Android

Length of output: 136


🏁 Script executed:

#!/bin/bash
# Check the domain layer for error mapping
find . -path "*/data/*/auth/*" -name "*Repository*.kt" | head -5 | xargs grep -l "PasswordMismatch"

Repository: team-aliens/DMS-Android

Length of output: 51


🏁 Script executed:

#!/bin/bash
# Search for suspendRunCatching - might be in shared exception util
rg "suspendRunCatching" --glob "*.kt" -B 2 -A 10 | head -50

Repository: team-aliens/DMS-Android

Length of output: 7093


🏁 Script executed:

#!/bin/bash
# Look for where suspendRunCatching is imported from
rg "suspendRunCatching" --glob "*.kt" -B 5 | grep -E "(import|package)" | head -20

Repository: team-aliens/DMS-Android

Length of output: 3484


🏁 Script executed:

#!/bin/bash
# Check the recent code review commit for dev file
git show d304aac79 -- "network/src/dev/kotlin/team.aliens.dms.android.network/user/datasource/NetworkUserDataSourceImpl.kt"

Repository: team-aliens/DMS-Android

Length of output: 1416


🏁 Script executed:

#!/bin/bash
# Look for domain layer AuthRepository to see if it handles 401 β†’ PasswordMismatchException
cat -n data/src/prod/kotlin/team/aliens/dms/android/data/auth/repository/AuthRepositoryImpl.kt | grep -A 20 -B 5 "onUnauthorized\|comparePassword"

Repository: team-aliens/DMS-Android

Length of output: 1131


🏁 Script executed:

#!/bin/bash
# Get the full suspendRunCatching implementation
cat -n shared/exception/src/main/java/team/aliens/dms/android/shared/exception/util/SuspendRunCatching.kt

Repository: team-aliens/DMS-Android

Length of output: 454


🏁 Script executed:

#!/bin/bash
# Check if there's a comparePassword in the domain layer and how it handles exceptions
rg "comparePassword" --glob "*.kt" -B 3 -A 10

Repository: team-aliens/DMS-Android

Length of output: 10965


🏁 Script executed:

#!/bin/bash
# Find the statusMapping function to understand domain error handling
rg "fun statusMapping" --glob "*.kt" -A 20 | head -80

Repository: team-aliens/DMS-Android

Length of output: 49


🏁 Script executed:

#!/bin/bash
# Check if there's diff/history showing when prod switched away from Result pattern
git log --oneline --all -- "network/src/prod/kotlin/team.aliens.dms.android.network/user/datasource/NetworkUserDataSourceImpl.kt" | head -20

Repository: team-aliens/DMS-Android

Length of output: 141


🏁 Script executed:

#!/bin/bash
# Look at prod repository to confirm it doesn't use Result pattern
cat -n data/src/prod/kotlin/team/aliens/dms/android/data/user/repository/UserRepositoryImpl.kt | head -40

Repository: team-aliens/DMS-Android

Length of output: 1076


Implement 401 β†’ PasswordMismatchException mapping in dev flavor.

The dev flavor lacks the 401 error mapping that exists in the prod implementation. With suspendRunCatching, a 401 will wrap as Result.failure(HttpException), losing the semantic distinction that prod provides via handleNetworkRequest(on401 = { throw PasswordMismatchException() }). This breaks consistency between flavors and prevents features from distinguishing password failures from other HTTP errors.

Add exception recovery to preserve the mapping:

Suggested fix
+import retrofit2.HttpException
+import team.aliens.dms.android.network.user.exception.PasswordMismatchException
@@
     override suspend fun comparePassword(password: String): Result<Unit> =
         suspendRunCatching {
             userApiService.comparePassword(password)
         }
+        .recoverCatching { e ->
+            if (e is HttpException && e.code() == 401) throw PasswordMismatchException()
+            throw e
+        }
πŸ€– Prompt for AI Agents
In
`@network/src/dev/kotlin/team.aliens.dms.android.network/user/datasource/NetworkUserDataSourceImpl.kt`
around lines 16 - 19, The dev implementation of comparePassword in
NetworkUserDataSourceImpl uses suspendRunCatching which preserves a 401 as
HttpException instead of mapping it to PasswordMismatchException; change the
implementation of comparePassword to recover 401 responses and throw
PasswordMismatchException (e.g. catch HttpException with statusCode 401 or use
Result.recover/exception mapping) so that comparePassword(password: String)
returns Result.failure(PasswordMismatchException()) for 401 while preserving
other errors unchanged; reference the comparePassword method, suspendRunCatching
call, and PasswordMismatchException when making the change.

@uson1004 uson1004 merged commit 4def3b4 into develop Jan 19, 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.

μ•Œλ¦Ό 및 곡지 ν™”λ©΄ κ΅¬ν˜„

1 participant