Skip to content

Feat#60 데모데이 전 QA#61

Merged
DongChyeon merged 21 commits intodevelopfrom
feat#60-qa-before-demo
Feb 26, 2026
Merged

Feat#60 데모데이 전 QA#61
DongChyeon merged 21 commits intodevelopfrom
feat#60-qa-before-demo

Conversation

@DongChyeon
Copy link
Member

@DongChyeon DongChyeon commented Feb 24, 2026

🛠 Related issue

closed #60

어떤 변경사항이 있었나요?

  • 🐞 BugFix Something isn't working
  • 🎨 Design Markup & styling
  • 📃 Docs Documentation writing and editing (README.md, etc.)
  • ✨ Feature Feature
  • 🔨 Refactor Code refactoring
  • ⚙️ Setting Development environment setup
  • ✅ Test Test related (Junit, etc.)

✅ CheckPoint

PR이 다음 요구 사항을 충족하는지 확인하세요.

  • PR 컨벤션에 맞게 작성했습니다. (필수)
  • merge할 브랜치의 위치를 확인해 주세요(main❌/develop⭕) (필수)
  • Approve된 PR은 assigner가 머지하고, 수정 요청이 온 경우 수정 후 다시 push를 합니다. (필수)
  • BugFix의 경우, 버그의 원인을 파악하였습니다. (선택)

✏️ Work Description

  • 알림 상세 화면 피드 삭제/신고 로직 적용: 홈 화면과 동일하게 알림 상세 화면에서도 피드 삭제 및 신고가 가능하도록 기능을 추가했습니다.
  • 가격 포맷팅 로직 리팩토링: Recomposition 시 반복되는 String.format 호출을 방지하기 위해 Repository 계층(DTO to Domain 변환 시점)에서 포맷팅을 처리하도록 변경했습니다.
  • 투표 결과 프로필 이미지 연동: FeedCard의 투표 결과 영역에서 기존 회색 원 대신 투표한 유저(현재 사용자)의 프로필 이미지가 표시되도록 DataStore 및 UI를 업데이트했습니다.
  • 최초 실행 시 알림 권한 요청: 앱 설치 후 최초 실행 시에만 알림 권한 팝업이 나타나도록 isFirstRun 플래그를 DataStore에 추가하고 로직을 개선했습니다.

😅 Uncompleted Tasks

  • N/A

📢 To Reviewers

  • domain 모듈이 core:datastore에 직접 의존하지 않도록 도메인 전용 UserPreferencesUserToken 모델을 정의하고 UserPreferencesRepository를 통해 추상화했습니다.
  • BuyOrNotViewModel을 신설하여 앱 전역 상태(최초 실행 여부 등)를 관리하도록 했습니다.
  • 졸리다...

📃 RCA 룰

  • R: 꼭 반영해 주세요. 적극적으로 고려해 주세요. (Request changes)
  • C: 웬만하면 반영해 주세요. (Comment)
  • A: 반영해도 좋고 넘어가도 좋습니다. 그냥 사소한 의견입니다. (Approve)

Summary by CodeRabbit

릴리스 노트

  • New Features

    • Firebase Crashlytics 도입으로 충돌 보고 지원
    • 피드·알림 상세에 투표자 프로필 이미지 표시
    • 로그인 후 프로필(닉네임·이미지) 자동 저장
    • 첫 실행에만 알림 권한 요청
  • Improvements

    • 업로드 화면 뒤로가기/종료 동작 개선(입력 유무 기반)
    • 초기 실행 플래그 도입으로 첫 실행 흐름 제어
  • Bug Fixes

    • 가격 표시 방식 변경(로컬 형식 문자열)
  • Chores

    • 앱 버전 업데이트 및 릴리스 빌드 난독화·리소스 축소 강화
    • ProGuard 규칙 정비 및 Crashlytics 구성 추가
    • 광고 ID 권한 추가

@DongChyeon DongChyeon self-assigned this Feb 24, 2026
@DongChyeon DongChyeon added ✨ FEAT 기능 개발 (애매하면 기능 개발로 두도록 하자) 💪 동현동현동현 labels Feb 24, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 24, 2026

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Firebase Crashlytics 통합·버전 상향, 릴리즈 난독화 강화, AD_ID 권한 추가, isFirstRun·UserPreferences·프로필 이미지 흐름 도입, Feed.price 타입 Int→String 변경, 본인 투표 차단 및 투표자 프로필 노출, 알림 상세의 삭제·신고·스낵바 흐름 추가.

Changes

Cohort / File(s) Summary
빌드·플러그인·프로가드
build.gradle.kts, app/build.gradle.kts, gradle/libs.versions.toml, app/proguard-rules.pro
Firebase Crashlytics 플러그인/라이브러리·버전 추가, Firebase BOM 업그레이드, ProGuard 규칙 대폭 추가, 릴리스 빌드 난독화(isMinifyEnabled/isShrinkResources) 활성화, 앱 버전 증분
매니페스트 권한
app/src/main/AndroidManifest.xml
com.google.android.gms.permission.AD_ID uses-permission 추가
앱 초기 실행 / 전역 상태
app/src/main/java/.../BuyOrNotApp.kt, app/src/main/java/.../BuyOrNotViewModel.kt, core/data/.../AppPreferencesRepositoryImpl.kt, core/datastore/.../AppPreferences*.kt, domain/.../AppPreferencesRepository.kt
isFirstRun 흐름 및 업데이트 API 추가, BuyOrNotApp이 ViewModel 주입으로 최초 실행 시 알림 권한 요청 처리 변경
도메인·데이터 모델·리포지토리
domain/src/.../Feed.kt, domain/src/.../UserPreferences.kt, domain/src/.../UserToken.kt, domain/.../UserPreferencesRepository.kt, core/data/.../UserPreferencesRepositoryImpl.kt, core/data/.../FeedRepositoryImpl.kt
Feed.price 타입 Int→String 변경(포맷 적용), UserPreferences/UserToken 도메인 추가, repository에 userPreferences/userToken 흐름 및 업데이트 메서드 추가
DataStore 계층
core/datastore/.../UserPreferences*.kt, core/datastore/.../AppPreferences*.kt, core/datastore/.../UserPreferencesDataSource*.kt, core/datastore/.../AppPreferencesDataSource*.kt
profileImageUrl 필드 및 키/읽기/쓰기(updateProfileImageUrl) 추가, isFirstRun 키·Flow·갱신 구현
디자인시스템·홈 UI
core/designsystem/.../FeedCard.kt, feature/home/.../ui/HomeContract.kt, feature/home/.../ui/HomeScreen.kt, feature/home/.../ui/HomeViewModel.kt
FeedCard·HomeUiState에 voterProfileImageUrl 추가로 투표자 프로필 노출, HomeViewModel이 userPreferences 관찰로 전환 및 본인 투표 차단 로직 추가
인증·마이페이지
feature/auth/.../LoginViewModel.kt, feature/mypage/.../MyPageViewModel.kt
로그인 성공 후 사용자 프로필 조회 및 UserPreferences에 displayName·profileImageUrl 저장 추가, MyPageViewModel에 UserPreferencesRepository 주입
알림 상세(삭제·신고·스낵바)
feature/notification/.../NotificationDetailContract.kt, .../NotificationDetailScreen.kt, .../NotificationDetailViewModel.kt
NotificationDetail에 voterProfileImageUrl/isOwner 추가, 삭제·신고 Intent 및 ShowSnackbar/NavigateBack 사이드이펙트 도입, ViewModel에서 삭제/신고 처리 후 스낵바·네비게이션 트리거 구현, 화면에서 SnackbarHostState 연결
업로드 화면 UX / 뒤로가기
feature/upload/.../UploadContract.kt, feature/upload/.../UploadScreen.kt
UploadUiState.hasInput 파생 속성 추가, 뒤로가기 동작을 hasInput에 따라 조건부 변경
네비게이션 import 정리
app/src/.../BuyOrNotNavHost.kt, feature/home/.../HomeNavigation.kt
HomeTab import 경로 viewmodel → ui로 변경

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

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • Imagine-Choi
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목이 '데모데이 전 QA'로 변경 사항의 주요 목표를 잘 요약하고 있으며, 연결된 이슈 #60과도 일치합니다.
Linked Issues check ✅ Passed PR은 연결된 이슈 #60의 모든 코딩 요구사항을 충족합니다: (1) 피드 상세 화면에 삭제/신고 기능 추가, (2) 투표 결과에 본인 프로필 이미지 표시, (3) 가격 포맷팅에 천 단위 구분 기호 적용.
Out of Scope Changes check ✅ Passed PR의 대부분 변경사항은 이슈 #60의 범위 내입니다. 다만 Firebase Crashlytics 추가와 ProGuard 규칙 확장은 QA 안정화의 일부로 볼 수 있으나, 이는 이슈에 명시되지 않은 추가 개선사항입니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat#60-qa-before-demo

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.

@DongChyeon DongChyeon marked this pull request as ready for review February 25, 2026 13:09
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: 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 | 🟡 Minor

TopBar 뒤로가기 핸들러에 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: 도메인 모델에서 priceString으로 변경하면 비즈니스 로직 활용이 제한됩니다.

가격을 도메인 레이어에서 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 형태로 기본값을 참조하는데, isFirstRuntrue로 하드코딩되어 있습니다. 추후 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

📥 Commits

Reviewing files that changed from the base of the PR and between c08452f and f902913.

📒 Files selected for processing (29)
  • app/build.gradle.kts
  • app/proguard-rules.pro
  • app/src/main/AndroidManifest.xml
  • app/src/main/java/com/sseotdabwa/buyornot/navigation/BuyOrNotNavHost.kt
  • app/src/main/java/com/sseotdabwa/buyornot/ui/BuyOrNotApp.kt
  • app/src/main/java/com/sseotdabwa/buyornot/ui/BuyOrNotViewModel.kt
  • build.gradle.kts
  • core/data/src/main/java/com/sseotdabwa/buyornot/core/data/repository/FeedRepositoryImpl.kt
  • core/data/src/main/java/com/sseotdabwa/buyornot/core/data/repository/UserPreferencesRepositoryImpl.kt
  • core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferences.kt
  • core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSource.kt
  • core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSourceImpl.kt
  • core/designsystem/src/main/java/com/sseotdabwa/buyornot/core/designsystem/components/FeedCard.kt
  • domain/src/main/java/com/sseotdabwa/buyornot/domain/model/Feed.kt
  • domain/src/main/java/com/sseotdabwa/buyornot/domain/model/UserPreferences.kt
  • domain/src/main/java/com/sseotdabwa/buyornot/domain/model/UserToken.kt
  • domain/src/main/java/com/sseotdabwa/buyornot/domain/repository/UserPreferencesRepository.kt
  • feature/auth/src/main/java/com/sseotdabwa/buyornot/feature/auth/ui/LoginViewModel.kt
  • feature/home/src/main/java/com/sseotdabwa/buyornot/feature/home/navigation/HomeNavigation.kt
  • feature/home/src/main/java/com/sseotdabwa/buyornot/feature/home/ui/HomeContract.kt
  • feature/home/src/main/java/com/sseotdabwa/buyornot/feature/home/ui/HomeScreen.kt
  • feature/home/src/main/java/com/sseotdabwa/buyornot/feature/home/ui/HomeViewModel.kt
  • feature/mypage/src/main/java/com/sseotdabwa/buyornot/feature/mypage/viewmodel/MyPageViewModel.kt
  • feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailContract.kt
  • feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailScreen.kt
  • feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailViewModel.kt
  • feature/upload/src/main/java/com/sseotdabwa/buyornot/feature/upload/ui/UploadContract.kt
  • feature/upload/src/main/java/com/sseotdabwa/buyornot/feature/upload/ui/UploadScreen.kt
  • gradle/libs.versions.toml

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

📥 Commits

Reviewing files that changed from the base of the PR and between f902913 and 29d3b29.

📒 Files selected for processing (3)
  • feature/auth/src/main/java/com/sseotdabwa/buyornot/feature/auth/ui/LoginViewModel.kt
  • feature/mypage/src/main/java/com/sseotdabwa/buyornot/feature/mypage/viewmodel/MyPageViewModel.kt
  • feature/notification/src/main/java/com/sseotdabwa/buyornot/feature/notification/ui/NotificationDetailViewModel.kt

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.

♻️ 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

📥 Commits

Reviewing files that changed from the base of the PR and between 29d3b29 and 2ac8fb4.

📒 Files selected for processing (2)
  • app/proguard-rules.pro
  • feature/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,
Copy link
Member

Choose a reason for hiding this comment

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

C: 여기서 혹시 price를 String으로 관리하는 이유가 뭔가요?

Copy link
Member Author

Choose a reason for hiding this comment

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

투표 피드에서 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")
Copy link
Member

Choose a reason for hiding this comment

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

C: 해당 부분 UserPreferences 보다는 App쪽에 저장하는게 좋지 않을까요?
앱을 지웠다 깔면 첫 실행시에 알림 권한 요청 모달 또 떠야하는 App 쪽에 속하는 정보 같아서...!!!!

Copy link
Member Author

Choose a reason for hiding this comment

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

5ff9472
수정했습니다.


init {
observeUserType()
observeUserPreferences()
Copy link
Member

Choose a reason for hiding this comment

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

굿굿!!

Comment on lines 101 to 169
@@ -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(
}
}
Copy link
Member

Choose a reason for hiding this comment

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

C: 프로필 정보 업데이트와 FCM 토큰 업데이트 병렬로 처리하는 건 어떨까요?

.onSuccess {
    viewModelScope.launch {
        launch { fetchAndStoreUserProfile() }
        launch { updateFcmToken() }
        
        sendSideEffect(LoginSideEffect.NavigateToHome)
    }
}

Copy link
Member Author

Choose a reason for hiding this comment

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

현재 fetchAndStoreUserProfile을 launch가 아닌 suspend 함수로 설정했는데요.
sendSideEffect으로 홈으로 이동시에 로그인 화면에 대한 백스택까지 날려버리기 때문에
LoginViewModel도 해제되어 중간에 하던 작업이 날라갈 수도 있습니다.

launch는 fire-and-forgot 방식이기 때문에 실행해놓고 동기 방식이 아니여서
suspend로 만들어서 앞선 fetchAndStoreUserProfile, updateFcmToken이 호출되고 네비게이션 이동이 되도록 보장해야 된다고 생각했습니다!

Copy link
Member Author

@DongChyeon DongChyeon Feb 26, 2026

Choose a reason for hiding this comment

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

dc2ecde
updateFcmToken도 해당 문제가 발생할 수 있을 것 같아 suspend 함수로 수정했습니다.

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

🧹 Nitpick comments (1)
core/data/src/main/java/com/sseotdabwa/buyornot/core/data/repository/UserPreferencesRepositoryImpl.kt (1)

21-25: 동일한 소스 Flow에서 두 개의 Flow가 파생됩니다.

userPreferencesuserToken 모두 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

📥 Commits

Reviewing files that changed from the base of the PR and between 2ac8fb4 and 5ff9472.

📒 Files selected for processing (13)
  • app/src/main/java/com/sseotdabwa/buyornot/ui/BuyOrNotViewModel.kt
  • core/data/src/main/java/com/sseotdabwa/buyornot/core/data/repository/AppPreferencesRepositoryImpl.kt
  • core/data/src/main/java/com/sseotdabwa/buyornot/core/data/repository/UserPreferencesRepositoryImpl.kt
  • core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/AppPreferences.kt
  • core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/AppPreferencesDataSource.kt
  • core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/AppPreferencesDataSourceImpl.kt
  • core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferences.kt
  • core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSource.kt
  • core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSourceImpl.kt
  • domain/src/main/java/com/sseotdabwa/buyornot/domain/model/UserPreferences.kt
  • domain/src/main/java/com/sseotdabwa/buyornot/domain/repository/AppPreferencesRepository.kt
  • domain/src/main/java/com/sseotdabwa/buyornot/domain/repository/UserPreferencesRepository.kt
  • feature/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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 5ff9472 and ed85932.

📒 Files selected for processing (8)
  • core/data/src/main/java/com/sseotdabwa/buyornot/core/data/repository/AuthRepositoryImpl.kt
  • core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/AppPreferencesDataSourceImpl.kt
  • core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSource.kt
  • core/datastore/src/main/java/com/sseotdabwa/buyornot/core/datastore/UserPreferencesDataSourceImpl.kt
  • core/network/src/main/java/com/sseotdabwa/buyornot/core/network/authenticator/TokenAuthenticator.kt
  • domain/src/main/java/com/sseotdabwa/buyornot/domain/repository/AuthRepository.kt
  • feature/mypage/src/main/java/com/sseotdabwa/buyornot/feature/mypage/viewmodel/AccountSettingViewModel.kt
  • feature/mypage/src/main/java/com/sseotdabwa/buyornot/feature/mypage/viewmodel/WithdrawalViewModel.kt

@DongChyeon DongChyeon merged commit c8db3da into develop Feb 26, 2026
2 checks passed
@DongChyeon DongChyeon deleted the feat#60-qa-before-demo branch February 26, 2026 14:31
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.

✨ Feature - 데모데이 전 QA

2 participants