Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughFirebase Crashlytics 통합·버전 상향, 릴리즈 난독화 강화, AD_ID 권한 추가, isFirstRun·UserPreferences·프로필 이미지 흐름 도입, Feed.price 타입 Int→String 변경, 본인 투표 차단 및 투표자 프로필 노출, 알림 상세의 삭제·신고·스낵바 흐름 추가. Changes
Sequence Diagram(s)sequenceDiagram
participant UI as NotificationDetailScreen
participant VM as NotificationDetailViewModel
participant Repo as FeedRepository
participant API as Network/API
UI->>VM: OnDeleteClicked
VM->>Repo: deleteFeed(feedId)
Repo->>API: DELETE /feeds/{id}
API-->>Repo: 200 OK
Repo-->>VM: success
VM-->>UI: ShowSnackbar("삭제되었습니다.", icon=IconResource(check))
VM-->>UI: NavigateBack
sequenceDiagram
participant UI as NotificationDetailScreen
participant VM as NotificationDetailViewModel
participant Repo as ReportRepository
participant API as Network/API
UI->>VM: OnReportClicked
VM->>Repo: reportFeed(feedId)
Repo->>API: POST /reports {feedId}
API-->>Repo: 201 / 400 / 5xx
Repo-->>VM: result
alt success
VM-->>UI: ShowSnackbar("신고가 접수되었습니다.")
else client error (400)
VM-->>UI: ShowSnackbar("이미 신고된 게시물입니다.")
else other error
VM-->>UI: ShowSnackbar("신고 중 오류가 발생했습니다.")
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
feature/upload/src/main/java/com/sseotdabwa/buyornot/feature/upload/ui/UploadScreen.kt (1)
121-143:⚠️ Potential issue | 🟡 MinorTopBar 뒤로가기 핸들러에
showExitDialog중복 디스패치 방지 가드 누락
BackHandler(Line 123)는!uiState.showExitDialog조건으로 이미 다이얼로그가 표시된 상태에서 인텐트를 중복 발행하지 않도록 보호하지만,BackTopBar람다(Line 139)에는 동일한 가드가 없습니다. 다이얼로그가 열린 상태에서 상단 바 뒤로가기 버튼을 누르면UpdateExitDialogVisibility(true)가 불필요하게 재발행됩니다.🔧 수정 제안
BackTopBar { if (uiState.hasInput) { - viewModel.handleIntent(UploadIntent.UpdateExitDialogVisibility(true)) + if (!uiState.showExitDialog) viewModel.handleIntent(UploadIntent.UpdateExitDialogVisibility(true)) } else { onNavigateBack() } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@feature/upload/src/main/java/com/sseotdabwa/buyornot/feature/upload/ui/UploadScreen.kt` around lines 121 - 143, BackTopBar's lambda can re-dispatch UpdateExitDialogVisibility(true) even when the dialog is already shown; mirror the guard used in BackHandler by checking uiState.showExitDialog before dispatching. Update the BackTopBar click handler to call viewModel.handleIntent(UploadIntent.UpdateExitDialogVisibility(true)) only when !uiState.showExitDialog, otherwise call onNavigateBack() as currently done; reference the BackTopBar lambda, BackHandler, uiState.showExitDialog, viewModel.handleIntent(UploadIntent.UpdateExitDialogVisibility(true)) and onNavigateBack() when making the change.
🧹 Nitpick comments (5)
core/designsystem/src/main/java/com/sseotdabwa/buyornot/core/designsystem/components/FeedCard.kt (1)
269-278:voterProfileImageUrl이 빈 문자열일 때 폴백 처리가 없습니다.
voterProfileImageUrl의 기본값이""이므로, 프로필 이미지 URL이 아직 로드되지 않았거나 없는 경우AsyncImage가 빈 URL을 요청하게 됩니다. 이전의 회색 원(Box)을 placeholder/error 상태로 활용하면 UX가 개선됩니다.또한 두 투표 옵션(Line 269-278, 293-302)에서 동일한
AsyncImage블록이 반복됩니다. 헬퍼 컴포저블로 추출하면 유지보수성이 향상됩니다.♻️ 제안: 공통 컴포저블 추출 및 placeholder 추가
+@Composable +private fun VoterProfileImage(voterProfileImageUrl: String) { + if (voterProfileImageUrl.isNotEmpty()) { + AsyncImage( + model = voterProfileImageUrl, + contentDescription = null, + modifier = Modifier + .size(20.dp) + .clip(CircleShape), + contentScale = ContentScale.Crop, + ) + } else { + Box( + modifier = Modifier + .size(20.dp) + .clip(CircleShape) + .background(BuyOrNotTheme.colors.gray400), + ) + } +}그리고 각
leadingContent블록에서:- AsyncImage( - model = voterProfileImageUrl, - contentDescription = null, - modifier = Modifier - .height(20.dp) - .width(20.dp) - .clip(CircleShape), - contentScale = ContentScale.Crop, - ) + VoterProfileImage(voterProfileImageUrl)Also applies to: 293-302
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@core/designsystem/src/main/java/com/sseotdabwa/buyornot/core/designsystem/components/FeedCard.kt` around lines 269 - 278, The AsyncImage usage for voterProfileImageUrl lacks placeholder/error handling and is duplicated; update the leadingContent blocks (the AsyncImage calls at the two locations) to show the existing gray circular Box when voterProfileImageUrl is blank or image loading fails, and extract the repeated AsyncImage + placeholder logic into a reusable composable (e.g., VoterAvatar or VoterProfileImage) that accepts the URL, size, and modifiers; replace both inline AsyncImage blocks with calls to that new composable and ensure it uses placeholder and error states (or conditional rendering when url.isBlank()) so the gray circle is shown until a valid image loads.domain/src/main/java/com/sseotdabwa/buyornot/domain/model/Feed.kt (1)
9-9: 도메인 모델에서price를String으로 변경하면 비즈니스 로직 활용이 제한됩니다.가격을 도메인 레이어에서
String으로 관리하면 향후 가격 비교, 합산, 할인 등 비즈니스 로직에서 다시 파싱해야 합니다. Recomposition 시 반복되는String.format호출을 방지하려는 의도는 이해하지만, 보다 일반적인 접근은 다음과 같습니다:
- 도메인 모델은
Int(또는Long)를 유지- UI 레이어에서
remember를 사용하여 포맷팅 결과를 캐싱val formattedPrice = remember(feed.price) { String.format(Locale.KOREA, "%,d", feed.price) }현재 앱 규모에서 즉시 문제가 되지는 않으므로, 향후 리팩토링 시 참고하시기 바랍니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@domain/src/main/java/com/sseotdabwa/buyornot/domain/model/Feed.kt` at line 9, Change the Feed domain model's price property from String back to a numeric type (Int or Long) in the Feed data class (property: price) so business logic can perform arithmetic/compare operations; update any code that constructs Feed instances to provide numeric values and adjust serialization/deserialization accordingly; keep formatting/locale presentation out of the domain and perform String formatting in the UI layer (use remember in the composable that renders Feed to cache formatted text, e.g. remember(feed.price) { String.format(...) }) so recomposition cost is avoided.app/proguard-rules.pro (1)
1-60: ProGuard 규칙이 과도하게 광범위하여 R8 최적화 효과가 감소합니다.Retrofit, OkHttp, Coil, Firebase 등 대부분의 라이브러리는 자체 consumer ProGuard 규칙을 AAR에 포함하고 있어 별도의 keep 규칙이 불필요합니다. 전체 패키지를
keep하면 R8의 코드 축소(shrinking) 및 최적화가 무력화됩니다.권장 접근:
- 먼저 라이브러리 자체 규칙에 의존하고, 릴리스 빌드 후 실제 크래시가 발생하는 클래스만 선별적으로 추가
-keepattributes *Annotation*이 Line 3, 21, 30에서 중복 선언되어 있으므로 정리 필요🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/proguard-rules.pro` around lines 1 - 60, The ProGuard rules are too broad and contain duplicates; remove blanket -keep clauses for libraries (e.g., "-keep class retrofit2.** { *; }", "-keep class okhttp3.** { *; }", "-keep class coil.** { *; }", "-keep class com.google.firebase.** { *; }") and rely on each AAR's consumer ProGuard rules, keep only truly required app model packages (keep the domain.model and core.network.model rules if they are necessary), consolidate/remove duplicate "-keepattributes *Annotation*" lines, and replace any full-package keeps with minimal, targeted rules (for example keep only specific classes or annotated members needed at runtime such as the existing "@retrofit2.http.* <methods>;" and kotlinx.serialization serializer preserves) so R8 can perform shrinking and optimization while preserving required runtime elements.core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSourceImpl.kt (1)
39-51:IS_FIRST_RUN기본값이 다른 필드와 패턴이 불일치합니다.다른 필드들은
UserPreferences().fieldName형태로 기본값을 참조하는데,isFirstRun만true로 하드코딩되어 있습니다. 추후UserPreferences데이터 클래스의 기본값이 변경되어도 이 코드에 자동으로 반영되지 않습니다.♻️ 패턴 일치를 위한 제안
- isFirstRun = prefs[Keys.IS_FIRST_RUN] ?: true, + isFirstRun = prefs[Keys.IS_FIRST_RUN] ?: UserPreferences().isFirstRun,추가로, 현재
map람다 안에서 필드마다UserPreferences()인스턴스를 별도로 생성하고 있습니다. 이벤트 방출마다 여러 번 객체를 생성하는 것을 피하려면 람다 시작 부분에 기본값 인스턴스를 한 번만 만들어 재사용하는 방식을 고려할 수 있습니다.♻️ 기본값 인스턴스 재사용 제안 (선택적)
override val preferences: Flow<UserPreferences> = context.userPreferencesDataStore.data.map { prefs -> + val defaults = UserPreferences() UserPreferences( - displayName = prefs[Keys.DISPLAY_NAME] ?: UserPreferences().displayName, - profileImageUrl = prefs[Keys.PROFILE_IMAGE_URL] ?: UserPreferences().profileImageUrl, - accessToken = prefs[Keys.ACCESS_TOKEN] ?: UserPreferences().accessToken, - refreshToken = prefs[Keys.REFRESH_TOKEN] ?: UserPreferences().refreshToken, + displayName = prefs[Keys.DISPLAY_NAME] ?: defaults.displayName, + profileImageUrl = prefs[Keys.PROFILE_IMAGE_URL] ?: defaults.profileImageUrl, + accessToken = prefs[Keys.ACCESS_TOKEN] ?: defaults.accessToken, + refreshToken = prefs[Keys.REFRESH_TOKEN] ?: defaults.refreshToken, userType = prefs[Keys.USER_TYPE]?.let { try { UserType.valueOf(it) } catch (e: IllegalArgumentException) { - UserPreferences().userType + defaults.userType } - } ?: UserPreferences().userType, + } ?: defaults.userType, - isFirstRun = prefs[Keys.IS_FIRST_RUN] ?: true, + isFirstRun = prefs[Keys.IS_FIRST_RUN] ?: defaults.isFirstRun, ) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSourceImpl.kt` around lines 39 - 51, The IS_FIRST_RUN default is hardcoded to true rather than reusing the data class default and the code repeatedly allocates UserPreferences(); update the mapping in UserPreferencesDataSourceImpl so it uses a single default = UserPreferences() instance at the start of the map lambda and replace the hardcoded true with default.isFirstRun (also change other field defaults to use that same default instance) so Keys.IS_FIRST_RUN reads prefs[Keys.IS_FIRST_RUN] ?: default.isFirstRun; keep references to the UserPreferences class and Keys.IS_FIRST_RUN to locate and modify the code.gradle/libs.versions.toml (1)
46-46: Firebase Crashlytics Gradle 플러그인을 최신 버전으로 업데이트하세요.공식 Firebase 문서 기준 최신 Crashlytics Gradle 플러그인 버전은
3.0.6(2025년 8월 7일 출시)인데, 현재3.0.3(2025년 2월 6일 출시)으로 고정되어 있습니다. 최신 버전에는 SDK 효율성 개선 사항이 포함되어 있습니다.⬆️ 제안하는 버전 업데이트
-firebaseCrashlytics = "3.0.3" +firebaseCrashlytics = "3.0.6"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@gradle/libs.versions.toml` at line 46, Update the pinned Firebase Crashlytics Gradle plugin version in libs.versions.toml: change the firebaseCrashlytics entry from "3.0.3" to the suggested "3.0.6" so the build uses the latest Crashlytics plugin; confirm no other references (e.g., pluginManagement or buildSrc lookups) override this value and run a build to verify compatibility.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/build.gradle.kts`:
- Around line 25-26: The versionName was unintentionally downgraded to "0.0.1"
while versionCode was incremented to 2; update the versionName in
build.gradle.kts (the versionName property alongside versionCode) to match the
intended semantic version (e.g., restore to "1.0.0" or bump to the correct next
semver) so Play Store/user-facing versioning remains consistent with
versionCode.
In `@app/proguard-rules.pro`:
- Around line 38-41: The ProGuard rule references a non-existent package
core.network.model so DTOs get obfuscated; update the keep rule to match the
real DTO package by replacing the keep entry for
com.sseotdabwa.buyornot.core.network.model.** with
com.sseotdabwa.buyornot.core.network.dto.** { *; } (you can also add explicit
keep lines for com.sseotdabwa.buyornot.core.network.dto.request.** and
com.sseotdabwa.buyornot.core.network.dto.response.** to be extra safe), locate
the relevant import usages in FeedRepositoryImpl to confirm package names and
ensure DTO classes are preserved for (de)serialization.
- Around line 58-60: The ProGuard rules reference a non-existent interface and
an internal class: remove the explicit keep for
androidx.compose.runtime.RecomposeScopeImpl and replace the incorrect interface
rule "-keep class * implements androidx.compose.runtime.Parcelable { *; }" with
a rule that targets the platform Parcelable (android.os.Parcelable) if you need
to keep parcelable implementations; e.g., delete the RecomposeScopeImpl keep
line and change the interface name from androidx.compose.runtime.Parcelable to
android.os.Parcelable in the proguard entries so only valid symbols
(RecomposeScopeImpl removed, use android.os.Parcelable) remain.
In `@app/src/main/AndroidManifest.xml`:
- Line 6: Remove the unnecessary AD_ID permission by adding the tools namespace
to the manifest element and using tools:node="remove" on the <uses-permission
android:name="com.google.android.gms.permission.AD_ID" /> entry, and disable
Advertising ID collection for Firebase Analytics by adding a <meta-data
android:name="google_analytics_adid_collection_enabled" android:value="false" />
inside the <application> element; reference the manifest's <uses-permission>
entry and the meta-data key "google_analytics_adid_collection_enabled" when
making the changes.
In `@app/src/main/java/com/sseotdabwa/buyornot/ui/BuyOrNotApp.kt`:
- Around line 63-72: The Boolean hasNotificationPermission returned from
rememberNotificationPermission() is a snapshot captured at composition and can
become stale inside LaunchedEffect(isFirstRun); update the code to read the live
permission state inside the coroutine instead of using the captured Boolean —
either change rememberNotificationPermission() to expose a State<Boolean> (e.g.,
hasNotificationPermissionState) and read hasNotificationPermissionState.value
inside LaunchedEffect, or query the current permission directly from
Context/Permission APIs inside the LaunchedEffect before calling
requestNotificationPermission(); ensure you still use
requestNotificationPermission() and call viewModel.updateIsFirstRun(false) after
the up-to-date permission check.
In
`@feature/mypage/src/main/java/com/sseotdabwa/buyornot/feature/mypage/viewmodel/MyPageViewModel.kt`:
- Around line 32-35: getMyProfile() is protected by runCatchingCancellable but
the subsequent DataStore calls userPreferencesRepository.updateDisplayName(...)
and updateProfileImageUrl(...) run outside that safety net and can throw
uncaught exceptions; wrap those DataStore updates inside a safe error-handling
block (e.g., runCatching or try/catch) within the same viewModelScope.launch
path that handles onSuccess for getMyProfile(), and on failure log the exception
and/or update state accordingly so failures in updateDisplayName or
updateProfileImageUrl don't crash the coroutine or silently get ignored.
In
`@feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailScreen.kt`:
- Line 148: The onVote callback passed in NotificationDetailScreen currently is
an empty lambda (onVote = { /* 이미 종료된 투표이기 때문에 투표 기능 미구현 */ }), so notifications
for feeds still in OPEN state won't trigger voting; update the logic in
NotificationDetailScreen (or the caller constructing the VoteView/row) to
forward a proper voting handler: detect the feed voteState (OPEN) and pass the
existing vote handling function (e.g., the same onVote used elsewhere or a new
handleVote(feedId, choice) method) instead of an empty lambda, and ensure the UI
disables the button only when the feed is CLOSED to avoid no-op behavior for
active polls.
In
`@feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailViewModel.kt`:
- Around line 60-64: 현재 profile 조회(userRepository.getMyProfile())와 feed
조회(feedRepository.getFeed(feedId))가 같은 runCatchingCancellable 블록에 있어 프로필 실패 시 상세
로드가 취소되므로, 프로필 조회는 별도 블록으로 분리하고 feed 조회는 독립적으로 실행하도록 변경하세요: 먼저
runCatchingCancellable로 userRepository.getMyProfile()를 호출해 성공 시 currentUserId를
설정(currentUserId = user.id)하되 실패하면 로그만 남기고 무시하고, feed 조회는 별도의
runCatchingCancellable에서 feedRepository.getFeed(feedId)를 호출해 피드 상세 로드를 항상 시도하게
하며 소유자 판별(owner check)은 currentUserId가 null인지 여부를 고려해 처리하도록 수정하세요 (참조:
runCatchingCancellable, currentUserId, userRepository.getMyProfile(),
feedRepository.getFeed(feedId)).
---
Outside diff comments:
In
`@feature/upload/src/main/java/com/sseotdabwa/buyornot/feature/upload/ui/UploadScreen.kt`:
- Around line 121-143: BackTopBar's lambda can re-dispatch
UpdateExitDialogVisibility(true) even when the dialog is already shown; mirror
the guard used in BackHandler by checking uiState.showExitDialog before
dispatching. Update the BackTopBar click handler to call
viewModel.handleIntent(UploadIntent.UpdateExitDialogVisibility(true)) only when
!uiState.showExitDialog, otherwise call onNavigateBack() as currently done;
reference the BackTopBar lambda, BackHandler, uiState.showExitDialog,
viewModel.handleIntent(UploadIntent.UpdateExitDialogVisibility(true)) and
onNavigateBack() when making the change.
---
Nitpick comments:
In `@app/proguard-rules.pro`:
- Around line 1-60: The ProGuard rules are too broad and contain duplicates;
remove blanket -keep clauses for libraries (e.g., "-keep class retrofit2.** { *;
}", "-keep class okhttp3.** { *; }", "-keep class coil.** { *; }", "-keep class
com.google.firebase.** { *; }") and rely on each AAR's consumer ProGuard rules,
keep only truly required app model packages (keep the domain.model and
core.network.model rules if they are necessary), consolidate/remove duplicate
"-keepattributes *Annotation*" lines, and replace any full-package keeps with
minimal, targeted rules (for example keep only specific classes or annotated
members needed at runtime such as the existing "@retrofit2.http.* <methods>;"
and kotlinx.serialization serializer preserves) so R8 can perform shrinking and
optimization while preserving required runtime elements.
In
`@core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSourceImpl.kt`:
- Around line 39-51: The IS_FIRST_RUN default is hardcoded to true rather than
reusing the data class default and the code repeatedly allocates
UserPreferences(); update the mapping in UserPreferencesDataSourceImpl so it
uses a single default = UserPreferences() instance at the start of the map
lambda and replace the hardcoded true with default.isFirstRun (also change other
field defaults to use that same default instance) so Keys.IS_FIRST_RUN reads
prefs[Keys.IS_FIRST_RUN] ?: default.isFirstRun; keep references to the
UserPreferences class and Keys.IS_FIRST_RUN to locate and modify the code.
In
`@core/designsystem/src/main/java/com/sseotdabwa/buyornot/core/designsystem/components/FeedCard.kt`:
- Around line 269-278: The AsyncImage usage for voterProfileImageUrl lacks
placeholder/error handling and is duplicated; update the leadingContent blocks
(the AsyncImage calls at the two locations) to show the existing gray circular
Box when voterProfileImageUrl is blank or image loading fails, and extract the
repeated AsyncImage + placeholder logic into a reusable composable (e.g.,
VoterAvatar or VoterProfileImage) that accepts the URL, size, and modifiers;
replace both inline AsyncImage blocks with calls to that new composable and
ensure it uses placeholder and error states (or conditional rendering when
url.isBlank()) so the gray circle is shown until a valid image loads.
In `@domain/src/main/java/com/sseotdabwa/buyornot/domain/model/Feed.kt`:
- Line 9: Change the Feed domain model's price property from String back to a
numeric type (Int or Long) in the Feed data class (property: price) so business
logic can perform arithmetic/compare operations; update any code that constructs
Feed instances to provide numeric values and adjust
serialization/deserialization accordingly; keep formatting/locale presentation
out of the domain and perform String formatting in the UI layer (use remember in
the composable that renders Feed to cache formatted text, e.g.
remember(feed.price) { String.format(...) }) so recomposition cost is avoided.
In `@gradle/libs.versions.toml`:
- Line 46: Update the pinned Firebase Crashlytics Gradle plugin version in
libs.versions.toml: change the firebaseCrashlytics entry from "3.0.3" to the
suggested "3.0.6" so the build uses the latest Crashlytics plugin; confirm no
other references (e.g., pluginManagement or buildSrc lookups) override this
value and run a build to verify compatibility.
ℹ️ Review info
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (29)
app/build.gradle.ktsapp/proguard-rules.proapp/src/main/AndroidManifest.xmlapp/src/main/java/com/sseotdabwa/buyornot/navigation/BuyOrNotNavHost.ktapp/src/main/java/com/sseotdabwa/buyornot/ui/BuyOrNotApp.ktapp/src/main/java/com/sseotdabwa/buyornot/ui/BuyOrNotViewModel.ktbuild.gradle.ktscore/data/src/main/java/com/sseotdabwa/buyornot/core/data/repository/FeedRepositoryImpl.ktcore/data/src/main/java/com/sseotdabwa/buyornot/core/data/repository/UserPreferencesRepositoryImpl.ktcore/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferences.ktcore/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSource.ktcore/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSourceImpl.ktcore/designsystem/src/main/java/com/sseotdabwa/buyornot/core/designsystem/components/FeedCard.ktdomain/src/main/java/com/sseotdabwa/buyornot/domain/model/Feed.ktdomain/src/main/java/com/sseotdabwa/buyornot/domain/model/UserPreferences.ktdomain/src/main/java/com/sseotdabwa/buyornot/domain/model/UserToken.ktdomain/src/main/java/com/sseotdabwa/buyornot/domain/repository/UserPreferencesRepository.ktfeature/auth/src/main/java/com/sseotdabwa/buyornot/feature/auth/ui/LoginViewModel.ktfeature/home/src/main/java/com/sseotdabwa/buyornot/feature/home/navigation/HomeNavigation.ktfeature/home/src/main/java/com/sseotdabwa/buyornot/feature/home/ui/HomeContract.ktfeature/home/src/main/java/com/sseotdabwa/buyornot/feature/home/ui/HomeScreen.ktfeature/home/src/main/java/com/sseotdabwa/buyornot/feature/home/ui/HomeViewModel.ktfeature/mypage/src/main/java/com/sseotdabwa/buyornot/feature/mypage/viewmodel/MyPageViewModel.ktfeature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailContract.ktfeature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailScreen.ktfeature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailViewModel.ktfeature/upload/src/main/java/com/sseotdabwa/buyornot/feature/upload/ui/UploadContract.ktfeature/upload/src/main/java/com/sseotdabwa/buyornot/feature/upload/ui/UploadScreen.ktgradle/libs.versions.toml
feature/auth/src/main/java/com/sseotdabwa/buyornot/feature/auth/ui/LoginViewModel.kt
Outdated
Show resolved
Hide resolved
...ure/mypage/src/main/java/com/sseotdabwa/buyornot/feature/mypage/viewmodel/MyPageViewModel.kt
Outdated
Show resolved
Hide resolved
...on/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailScreen.kt
Show resolved
Hide resolved
...src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailViewModel.kt
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailViewModel.kt (1)
59-86: 이전 리뷰 지적 사항 반영 확인 ✅프로필 조회를 별도의
runCatchingCancellable로 분리하여, 프로필 조회 실패 시에도 피드 상세 조회가 독립적으로 실행됩니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailViewModel.kt` around lines 59 - 86, The separate runCatchingCancellable around userRepository.getMyProfile() in loadDetail() correctly isolates profile fetch failures from feedRepository.getFeed(); keep the current structure (loadDetail(), currentUserId, userRepository.getMyProfile(), feedRepository.getFeed(), runCatchingCancellable, updateState) as-is—no further changes required.
🧹 Nitpick comments (1)
feature/mypage/src/main/java/com/sseotdabwa/buyornot/feature/mypage/viewmodel/MyPageViewModel.kt (1)
40-47: 경고 로그에Throwable을 함께 넘겨 스택트레이스를 보존해 주세요.Line 41, Line 46 모두 현재는 예외 객체 정보가 충분히 남지 않아 장애 원인 추적이 어려워집니다.
🔧 제안 코드
- }.onFailure { - Log.w(TAG, "Failed to update user preferences") + }.onFailure { throwable -> + Log.w(TAG, "Failed to update user preferences", throwable) } }.onFailure { throwable -> updateState { it.copy(isLoading = false) } sendSideEffect(MyPageSideEffect.ShowSnackbar("프로필을 불러오지 못했습니다.")) - Log.w(TAG, throwable.toString()) + Log.w(TAG, "Failed to load profile", throwable) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@feature/mypage/src/main/java/com/sseotdabwa/buyornot/feature/mypage/viewmodel/MyPageViewModel.kt` around lines 40 - 47, In MyPageViewModel update the failing lambda handlers to preserve stack traces by accepting the Throwable parameter and passing it to Log.w as the second argument; specifically change the first .onFailure block that currently logs only "Failed to update user preferences" to accept a throwable (e.g., .onFailure { throwable -> }) and call Log.w(TAG, "Failed to update user preferences", throwable), and change the later .onFailure { throwable -> Log.w(TAG, throwable.toString()) } to Log.w(TAG, "프로필을 불러오지 못했습니다.", throwable) so TAG and the exception are logged together.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailViewModel.kt`:
- Around line 88-138: Replace the hardcoded logger tag string in the Log.e calls
with the existing TAG constant: in NotificationDetailViewModel, update the
Log.e(...) invocations inside handleDelete and handleReport to use TAG instead
of "NotificationDetailViewModel" so both failure logs reference the single TAG
constant defined earlier in the class.
---
Duplicate comments:
In
`@feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailViewModel.kt`:
- Around line 59-86: The separate runCatchingCancellable around
userRepository.getMyProfile() in loadDetail() correctly isolates profile fetch
failures from feedRepository.getFeed(); keep the current structure
(loadDetail(), currentUserId, userRepository.getMyProfile(),
feedRepository.getFeed(), runCatchingCancellable, updateState) as-is—no further
changes required.
---
Nitpick comments:
In
`@feature/mypage/src/main/java/com/sseotdabwa/buyornot/feature/mypage/viewmodel/MyPageViewModel.kt`:
- Around line 40-47: In MyPageViewModel update the failing lambda handlers to
preserve stack traces by accepting the Throwable parameter and passing it to
Log.w as the second argument; specifically change the first .onFailure block
that currently logs only "Failed to update user preferences" to accept a
throwable (e.g., .onFailure { throwable -> }) and call Log.w(TAG, "Failed to
update user preferences", throwable), and change the later .onFailure {
throwable -> Log.w(TAG, throwable.toString()) } to Log.w(TAG, "프로필을 불러오지
못했습니다.", throwable) so TAG and the exception are logged together.
ℹ️ Review info
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
feature/auth/src/main/java/com/sseotdabwa/buyornot/feature/auth/ui/LoginViewModel.ktfeature/mypage/src/main/java/com/sseotdabwa/buyornot/feature/mypage/viewmodel/MyPageViewModel.ktfeature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailViewModel.kt
...src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailViewModel.kt
Show resolved
Hide resolved
There was a problem hiding this comment.
♻️ Duplicate comments (1)
feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailViewModel.kt (1)
112-138:⚠️ Potential issue | 🟡 Minor`` —
TAG상수 대신 문자열 리터럴 사용 (Line 124)Line 124의
Log.e호출에TAG상수 대신"NotificationDetailViewModel"문자열이 하드코딩되어 있습니다. 이전 리뷰에서 이미 지적된 사항이며 아직 수정되지 않았습니다.🔧 제안 수정안
- Log.e("NotificationDetailViewModel", "Failed to report feed: $feedId", e) + Log.e(TAG, "Failed to report feed: $feedId", e)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailViewModel.kt` around lines 112 - 138, Replace the hardcoded log tag string in the Log.e call inside handleReport() with a TAG constant: locate the Log.e usage in NotificationDetailViewModel.handleReport and change the first argument from "NotificationDetailViewModel" to a class-level TAG constant (e.g., private const val TAG = "NotificationDetailViewModel"); if TAG does not exist in NotificationDetailViewModel, add that private const val TAG at the top of the class so all logging uses the shared TAG constant.
🧹 Nitpick comments (1)
feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailViewModel.kt (1)
125-129: HTTP 400 에러를 메시지 문자열 파싱으로 감지하는 방식이 불안정합니다.
e.message?.contains("400")체크는 에러 메시지 포맷에 강하게 의존합니다. 네트워크 라이브러리(Retrofit 등)의 버전 변경이나 서버 응답 변경 시"400"문자열이 포함되지 않을 수 있으며, 반대로 다른 에러 메시지에"400"이 포함된 경우 오판할 수 있습니다.HttpException.code()등 타입 안전한 방식으로 상태 코드를 확인하는 것을 권장합니다.♻️ 제안 수정안
+ import retrofit2.HttpException }.onFailure { e -> Log.e(TAG, "Failed to report feed: $feedId", e) val errorMessage = when { - e.message?.contains("400") == true -> "이미 신고한 피드이거나 본인의 피드입니다." + e is HttpException && e.code() == 400 -> "이미 신고한 피드이거나 본인의 피드입니다." else -> "신고에 실패했습니다." }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailViewModel.kt` around lines 125 - 129, The current error handling infers HTTP 400 by checking e.message for "400", which is fragile; update the exception branch in NotificationDetailViewModel where errorMessage is built to type-check the caught exception (the variable e) for an instance of HttpException and use its code() (or statusCode) to detect 400 explicitly, setting the message to "이미 신고한 피드이거나 본인의 피드입니다." for code 400 and the default "신고에 실패했습니다." otherwise; ensure you import and reference retrofit2.HttpException (or your project's HTTP exception type) and avoid string parsing of e.message.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In
`@feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailViewModel.kt`:
- Around line 112-138: Replace the hardcoded log tag string in the Log.e call
inside handleReport() with a TAG constant: locate the Log.e usage in
NotificationDetailViewModel.handleReport and change the first argument from
"NotificationDetailViewModel" to a class-level TAG constant (e.g., private const
val TAG = "NotificationDetailViewModel"); if TAG does not exist in
NotificationDetailViewModel, add that private const val TAG at the top of the
class so all logging uses the shared TAG constant.
---
Nitpick comments:
In
`@feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailViewModel.kt`:
- Around line 125-129: The current error handling infers HTTP 400 by checking
e.message for "400", which is fragile; update the exception branch in
NotificationDetailViewModel where errorMessage is built to type-check the caught
exception (the variable e) for an instance of HttpException and use its code()
(or statusCode) to detect 400 explicitly, setting the message to "이미 신고한 피드이거나
본인의 피드입니다." for code 400 and the default "신고에 실패했습니다." otherwise; ensure you
import and reference retrofit2.HttpException (or your project's HTTP exception
type) and avoid string parsing of e.message.
ℹ️ Review info
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
app/proguard-rules.profeature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailViewModel.kt
🚧 Files skipped from review as they are similar to previous changes (1)
- app/proguard-rules.pro
| val feedId: Long, | ||
| val content: String, | ||
| val price: Int, | ||
| val price: String, |
There was a problem hiding this comment.
C: 여기서 혹시 price를 String으로 관리하는 이유가 뭔가요?
There was a problem hiding this comment.
투표 피드에서 price를 천 단위마다 , 기호를 통해 표시하게 되는데
이 포맷팅을 UI Composable에서 실행하게 되면 매 컴포지션마다 String.format을 실행하게 되어
네트워크 DTO -> 도메인 모델 변환 과정에서 String.fomat을 한번만 사용하도록 했슴당
| val ACCESS_TOKEN = stringPreferencesKey("access_token") | ||
| val REFRESH_TOKEN = stringPreferencesKey("refresh_token") | ||
| val USER_TYPE = stringPreferencesKey("user_type") | ||
| val IS_FIRST_RUN = booleanPreferencesKey("is_first_run") |
There was a problem hiding this comment.
C: 해당 부분 UserPreferences 보다는 App쪽에 저장하는게 좋지 않을까요?
앱을 지웠다 깔면 첫 실행시에 알림 권한 요청 모달 또 떠야하는 App 쪽에 속하는 정보 같아서...!!!!
|
|
||
| init { | ||
| observeUserType() | ||
| observeUserPreferences() |
| @@ -157,6 +158,7 @@ class LoginViewModel @Inject constructor( | |||
| runCatchingCancellable { | |||
| authRepository.kakaoLogin(accessToken) | |||
| }.onSuccess { | |||
| fetchAndStoreUserProfile() | |||
| updateFcmToken() | |||
| sendSideEffect(LoginSideEffect.NavigateToHome) | |||
| }.onFailure { | |||
| @@ -166,6 +168,17 @@ class LoginViewModel @Inject constructor( | |||
| } | |||
| } | |||
There was a problem hiding this comment.
C: 프로필 정보 업데이트와 FCM 토큰 업데이트 병렬로 처리하는 건 어떨까요?
.onSuccess {
viewModelScope.launch {
launch { fetchAndStoreUserProfile() }
launch { updateFcmToken() }
sendSideEffect(LoginSideEffect.NavigateToHome)
}
}
There was a problem hiding this comment.
현재 fetchAndStoreUserProfile을 launch가 아닌 suspend 함수로 설정했는데요.
sendSideEffect으로 홈으로 이동시에 로그인 화면에 대한 백스택까지 날려버리기 때문에
LoginViewModel도 해제되어 중간에 하던 작업이 날라갈 수도 있습니다.
launch는 fire-and-forgot 방식이기 때문에 실행해놓고 동기 방식이 아니여서
suspend로 만들어서 앞선 fetchAndStoreUserProfile, updateFcmToken이 호출되고 네비게이션 이동이 되도록 보장해야 된다고 생각했습니다!
There was a problem hiding this comment.
dc2ecde
updateFcmToken도 해당 문제가 발생할 수 있을 것 같아 suspend 함수로 수정했습니다.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
core/data/src/main/java/com/sseotdabwa/buyornot/core/data/repository/UserPreferencesRepositoryImpl.kt (1)
21-25: 동일한 소스 Flow에서 두 개의 Flow가 파생됩니다.
userPreferences와userToken모두userPreferencesDataSource.preferences에서 파생됩니다. 현재 구현은 정상 동작하지만, 두 Flow를 동시에 collect할 경우 DataStore에서 두 번 읽기가 발생할 수 있습니다. 성능이 중요한 경우shareIn을 사용하여 단일 upstream을 공유하는 것을 고려해볼 수 있습니다.현재 사용 패턴에서 문제가 되지 않는다면 그대로 유지해도 무방합니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@core/data/src/main/java/com/sseotdabwa/buyornot/core/data/repository/UserPreferencesRepositoryImpl.kt` around lines 21 - 25, userPreferences and userToken are independently mapping the same upstream userPreferencesDataSource.preferences, which can cause duplicate DataStore reads when both Flows are collected; change to share the upstream Flow (e.g., create a sharedPreferencesFlow from userPreferencesDataSource.preferences using shareIn or stateIn with an appropriate CoroutineScope and SharingStarted policy) and then map that sharedPreferencesFlow to produce userPreferences (via toDomain()) and userToken (via toTokenDomain()) so the DataStore is read only once.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/src/main/java/com/sseotdabwa/buyornot/ui/BuyOrNotViewModel.kt`:
- Around line 16-22: The initialValue passed to stateIn for isFirstRun does not
match the DataStore default (currently set to false while the stored default is
true), causing first-run logic to misfire; update the stateIn call on
appPreferencesRepository.isFirstRun (the isFirstRun property in
BuyOrNotViewModel) to use initialValue = true so it aligns with the DataStore
default, keeping the rest of the stateIn parameters (viewModelScope and
SharingStarted.WhileSubscribed(5000)) unchanged.
In
`@core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSourceImpl.kt`:
- Around line 28-29: The logout flow leaves PROFILE_IMAGE_URL in the DataStore
because clearTokens() only removes ACCESS_TOKEN; update clearTokens() in
UserPreferencesDataSourceImpl to also remove PROFILE_IMAGE_URL (and any other
user-scoped keys added, e.g., the profile image key referenced by
PROFILE_IMAGE_URL) so the previous user's profile image isn't retained after
logout—locate the stringPreferencesKey declarations (PROFILE_IMAGE_URL,
ACCESS_TOKEN) and ensure clearTokens() removes/profile-clears PROFILE_IMAGE_URL
alongside ACCESS_TOKEN.
---
Nitpick comments:
In
`@core/data/src/main/java/com/sseotdabwa/buyornot/core/data/repository/UserPreferencesRepositoryImpl.kt`:
- Around line 21-25: userPreferences and userToken are independently mapping the
same upstream userPreferencesDataSource.preferences, which can cause duplicate
DataStore reads when both Flows are collected; change to share the upstream Flow
(e.g., create a sharedPreferencesFlow from userPreferencesDataSource.preferences
using shareIn or stateIn with an appropriate CoroutineScope and SharingStarted
policy) and then map that sharedPreferencesFlow to produce userPreferences (via
toDomain()) and userToken (via toTokenDomain()) so the DataStore is read only
once.
ℹ️ Review info
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (13)
app/src/main/java/com/sseotdabwa/buyornot/ui/BuyOrNotViewModel.ktcore/data/src/main/java/com/sseotdabwa/buyornot/core/data/repository/AppPreferencesRepositoryImpl.ktcore/data/src/main/java/com/sseotdabwa/buyornot/core/data/repository/UserPreferencesRepositoryImpl.ktcore/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/AppPreferences.ktcore/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/AppPreferencesDataSource.ktcore/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/AppPreferencesDataSourceImpl.ktcore/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferences.ktcore/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSource.ktcore/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSourceImpl.ktdomain/src/main/java/com/sseotdabwa/buyornot/domain/model/UserPreferences.ktdomain/src/main/java/com/sseotdabwa/buyornot/domain/repository/AppPreferencesRepository.ktdomain/src/main/java/com/sseotdabwa/buyornot/domain/repository/UserPreferencesRepository.ktfeature/auth/src/main/java/com/sseotdabwa/buyornot/feature/auth/ui/LoginViewModel.kt
🚧 Files skipped from review as they are similar to previous changes (3)
- core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferences.kt
- feature/auth/src/main/java/com/sseotdabwa/buyornot/feature/auth/ui/LoginViewModel.kt
- domain/src/main/java/com/sseotdabwa/buyornot/domain/model/UserPreferences.kt
...astore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSourceImpl.kt
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/AppPreferencesDataSourceImpl.kt (1)
35-47:⚠️ Potential issue | 🟠 Major
isFirstRun기본값이 읽기 경로마다 다릅니다.Line 35는 기본값이
false인데, Line 46은 기본값이true라서 같은 키를 읽어도 호출 경로에 따라 최초 실행 판정이 달라집니다.🔧 제안 수정안
override val isFirstRun: Flow<Boolean> = context.appPreferencesDataStore.data.map { prefs -> - prefs[Keys.IS_FIRST_RUN] ?: true + prefs[Keys.IS_FIRST_RUN] ?: false }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/AppPreferencesDataSourceImpl.kt` around lines 35 - 47, The default for Keys.IS_FIRST_RUN is inconsistent: the data class mapping uses false while the standalone Flow isFirstRun uses true; update one so both read paths use the same default (pick the correct semantic default, e.g., true for first run) by changing the default literal in the isFirstRun Flow mapping that reads context.appPreferencesDataStore.data.map { prefs -> prefs[Keys.IS_FIRST_RUN] ?: true } (or change the other to ?: false) so Keys.IS_FIRST_RUN has a consistent fallback across both usages.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSourceImpl.kt`:
- Around line 93-99: clearUserInfo currently leaves the user's DISPLAY_NAME in
the DataStore so previous user names persist; update the edit block inside
clearUserInfo to remove Keys.DISPLAY_NAME as well (in the same prefs.remove(...)
group alongside Keys.ACCESS_TOKEN, Keys.REFRESH_TOKEN, Keys.PROFILE_IMAGE_URL)
while keeping the line that sets prefs[Keys.USER_TYPE] = UserType.GUEST.name.
---
Outside diff comments:
In
`@core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/AppPreferencesDataSourceImpl.kt`:
- Around line 35-47: The default for Keys.IS_FIRST_RUN is inconsistent: the data
class mapping uses false while the standalone Flow isFirstRun uses true; update
one so both read paths use the same default (pick the correct semantic default,
e.g., true for first run) by changing the default literal in the isFirstRun Flow
mapping that reads context.appPreferencesDataStore.data.map { prefs ->
prefs[Keys.IS_FIRST_RUN] ?: true } (or change the other to ?: false) so
Keys.IS_FIRST_RUN has a consistent fallback across both usages.
ℹ️ Review info
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
core/data/src/main/java/com/sseotdabwa/buyornot/core/data/repository/AuthRepositoryImpl.ktcore/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/AppPreferencesDataSourceImpl.ktcore/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSource.ktcore/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSourceImpl.ktcore/network/src/main/java/com/sseotdabwa/buyornot/core/network/authenticator/TokenAuthenticator.ktdomain/src/main/java/com/sseotdabwa/buyornot/domain/repository/AuthRepository.ktfeature/mypage/src/main/java/com/sseotdabwa/buyornot/feature/mypage/viewmodel/AccountSettingViewModel.ktfeature/mypage/src/main/java/com/sseotdabwa/buyornot/feature/mypage/viewmodel/WithdrawalViewModel.kt
🛠 Related issue
closed #60
어떤 변경사항이 있었나요?
✅ CheckPoint
PR이 다음 요구 사항을 충족하는지 확인하세요.
✏️ Work Description
String.format호출을 방지하기 위해 Repository 계층(DTO to Domain 변환 시점)에서 포맷팅을 처리하도록 변경했습니다.isFirstRun플래그를 DataStore에 추가하고 로직을 개선했습니다.😅 Uncompleted Tasks
📢 To Reviewers
domain모듈이core:datastore에 직접 의존하지 않도록 도메인 전용UserPreferences및UserToken모델을 정의하고UserPreferencesRepository를 통해 추상화했습니다.BuyOrNotViewModel을 신설하여 앱 전역 상태(최초 실행 여부 등)를 관리하도록 했습니다.📃 RCA 룰
Summary by CodeRabbit
릴리스 노트
New Features
Improvements
Bug Fixes
Chores