Skip to content

[Refactor/#192] DailyEmotion 데이터 흐름 개선#193

Merged
wjdrjs00 merged 22 commits intodevelopfrom
refactor/#192-daily-emotion-flow
Mar 15, 2026
Merged

[Refactor/#192] DailyEmotion 데이터 흐름 개선#193
wjdrjs00 merged 22 commits intodevelopfrom
refactor/#192-daily-emotion-flow

Conversation

@wjdrjs00
Copy link
Member

@wjdrjs00 wjdrjs00 commented Mar 8, 2026

[ PR Content ]

스크럼 시간에 언급했던 flow 기반의 데이터 스트림 방식을 DailyEmotion(오늘의 감정) 부분에 먼저 적용을 해봤습니다.
작업 문서는 노션에 띠로 정리했습니다.! -> 🔗 노션 링크

Related issue

Screenshot 📸

  • N/A

Work Description

  • EmotionRepositoryImpl.kt: StateFlow 도입, onSubscription 및 중복 호출 방지 로직 적용
  • DailyEmotion.kt: fetchedDate 필드 및 isStale 속성 추가를 통한 상태 판단 로직 내재화
  • HomeViewModel.kt / RecommendRoutineViewModel.kt: 명령형 로직 제거 및 Flow 관찰 방식으로 리팩터링
  • 삭제: EmotionChangeEvent.kt, FetchDailyEmotionUseCase.kt, GetEmotionChangeEventFlowUseCase.kt (레거시 제거)

To Reviewers 📢

  • 궁금한 사항이 있다면 리뷰 남겨주세요!

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 일일 감정 및 사용자 프로필을 스트림(구독)으로 관찰하는 API 도입.
  • 개선 사항

    • 구독 시 자동으로 최신 데이터 페치·반영 및 캐시 동기화(동시성 보호).
    • 일일 데이터에 조회 시점 표시(fetchedDate)와 신선도 판정 추가.
    • 로그아웃/탈퇴 시 로컬 캐시 자동 정리.
  • 제거

    • 기존 이벤트 기반 감정 변경 알림 경로 및 관련 사용 사례 삭제.

@wjdrjs00 wjdrjs00 requested a review from l5x5l March 8, 2026 17:47
@wjdrjs00 wjdrjs00 self-assigned this Mar 8, 2026
@wjdrjs00 wjdrjs00 added 🔨 Refactor 기존 기능 개선 🧤 대현 labels Mar 8, 2026
@coderabbitai
Copy link

coderabbitai bot commented Mar 8, 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

이벤트 기반 감정 갱신을 제거하고 StateFlow 기반 관찰로 전환했습니다. DailyEmotion에 fetchedDate/isStale가 추가되고, 저장소는 원격/로컬 분리, Mutex 동기화, 구독 시 자동 페치·캐시 로직을 구현합니다. ViewModel들은 ObserveDailyEmotionUseCase로 전환되었습니다.

Changes

Cohort / File(s) Summary
도메인 모델
domain/src/main/java/com/threegap/bitnagil/domain/emotion/model/DailyEmotion.kt
fetchedDate: LocalDate 추가, isStale(today: LocalDate)companion object { INIT } 추가.
응답 매핑
data/src/main/java/com/threegap/bitnagil/data/emotion/model/response/DailyEmotionResponse.kt
toDomain()toDomain(fetchedDate: LocalDate)로 시그니처 변경; 매핑 시 fetchedDate 전달.
저장소 구현 (Emotion)
data/src/main/java/com/threegap/bitnagil/data/emotion/repositoryImpl/EmotionRepositoryImpl.kt
원격/로컬 데이터소스 분리, observeDailyEmotion(): Flow<Result<DailyEmotion>> 추가, fetchAndSaveDailyEmotion(...)Mutex 동기화 도입, 이벤트 타입/SharedFlow 제거, clearCache() 추가. 생성자 의존성 변경.
데이터소스 인터페이스 (Emotion)
data/src/main/java/com/threegap/bitnagil/data/emotion/datasource/EmotionRemoteDataSource.kt, data/.../datasource/EmotionLocalDataSource.kt
인터페이스 분리(이름 변경: EmotionRemoteDataSource), 로컬 인터페이스에 val dailyEmotion: StateFlow<DailyEmotion?>, saveDailyEmotion, clearCache 추가.
데이터소스 구현 (Emotion)
data/.../datasourceImpl/EmotionRemoteDataSourceImpl.kt, data/.../datasourceImpl/EmotionLocalDataSourceImpl.kt
원격 구현명 변경(EmotionRemoteDataSourceImpl) 및 로컬 구현 추가(EmotionLocalDataSourceImpl)로 StateFlow 기반 캐시 구현.
저장소 구현 (User)
data/src/main/java/com/threegap/bitnagil/data/user/repositoryImpl/UserRepositoryImpl.kt
로컬/원격 분리, observeUserProfile(): Flow<Result<UserProfile>>, getUserProfile(), clearCache()fetchAndCacheIfNeeded()Mutex 도입.
데이터소스 (User)
data/src/main/java/com/threegap/bitnagil/data/user/datasource/*.kt, .../datasourceImpl/*.kt
UserDataSourceUserRemoteDataSourceUserLocalDataSource 분리; 로컬/원격 Impl 추가 및 바인딩 대응.
DI 모듈
app/src/main/java/com/threegap/bitnagil/di/data/DataSourceModule.kt
단일 데이터소스 바인딩을 로컬/원격 바인딩으로 분리(Emotion/User).
도메인 저장소/유스케이스 (Emotion)
domain/src/main/java/com/threegap/bitnagil/domain/emotion/repository/EmotionRepository.kt, domain/.../usecase/ObserveDailyEmotionUseCase.kt
fetchDailyEmotion 및 이벤트 흐름 제거, observeDailyEmotion(): Flow<Result<DailyEmotion>>clearCache() 추가. FetchDailyEmotionUseCaseObserveDailyEmotionUseCase로 변경(Flow 반환).
제거된 이벤트/유스케이스
domain/.../emotion/model/EmotionChangeEvent.kt, domain/.../emotion/usecase/GetEmotionChangeEventFlowUseCase.kt
EmotionChangeEvent 및 관련 이벤트 유스케이스 삭제.
프레젠테이션
presentation/src/main/java/com/threegap/bitnagil/presentation/screen/home/HomeViewModel.kt, .../recommendroutine/RecommendRoutineViewModel.kt, .../mypage/MyPageViewModel.kt, .../onboarding/OnBoardingViewModel.kt
ViewModel들이 이벤트 기반 의존성 제거 및 ObserveDailyEmotionUseCase/ObserveUserProfileUseCase로 전환, container 빌드설정(onCreate/repeatOnSubscription) 적용.
인증/계정 유스케이스
domain/src/main/java/com/threegap/bitnagil/domain/auth/usecase/{Logout,Withdrawal}UseCase.kt
로그아웃/탈퇴 성공 시 userRepository.clearCache()emotionRepository.clearCache() 호출 추가(의존성 주입 변경).
테스트 추가
data/src/test/java/com/threegap/bitnagil/data/user/repositoryImpl/UserRepositoryImplTest.kt
UserRepositoryImpl 동시성/캐시 동작 검증을 위한 단위테스트 및 Fake 데이터소스 추가.
빌드(테스트)
data/build.gradle.kts
테스트 의존성(junit, coroutines.test) 추가.

Sequence Diagram(s)

sequenceDiagram
    participant VM as ViewModel
    participant UC as ObserveDailyEmotionUseCase
    participant Repo as EmotionRepositoryImpl
    participant Remote as EmotionRemoteDataSource
    participant Local as EmotionLocalDataSource

    rect rgba(135,206,250,0.5)
    VM->>UC: 구독 시작
    end

    UC->>Repo: observeDailyEmotion() 구독
    Note right of Repo: onSubscription 트리거
    Repo->>Repo: Mutex로 동시성 검사
    alt 캐시가 없거나 isStale 또는 forceRefresh
        Repo->>Remote: 원격 API 호출
        Remote-->>Repo: 응답(감정 데이터)
        Repo->>Local: saveDailyEmotion(DailyEmotion with fetchedDate)
        Local-->>Repo: StateFlow 업데이트
        Repo-->>UC: Flow로 Result 발행
        UC-->>VM: UI 업데이트
    else 캐시가 최신
        Local-->>Repo: 현재 캐시값 반환
        Repo-->>UC: Flow로 Result 발행 (네트워크 없음)
        UC-->>VM: UI 업데이트
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • l5x5l

Poem

"깡총깡총, 깃발 들고 왔어요,
캐시도 지키고 Mutex 춤춰요.
StateFlow 물결에 감정이 번져,
화면마다 반짝이는 새 아침.
토끼가 응원해요: 갱신 잘했어요! 🐇"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 주요 변경사항인 DailyEmotion의 StateFlow 기반 데이터 흐름 개선을 명확하게 나타내고 있습니다.
Description check ✅ Passed PR 설명에서 Related issue, Work Description, To Reviewers 섹션이 포함되어 있으나, 템플릿의 Screenshot 섹션이 N/A로 처리되어 있습니다.
Linked Issues check ✅ Passed 모든 #192 이슈의 요구사항(StateFlow 도입, fetchedDate/isStale 추가, onSubscription 자동 로드, Mutex 동시성 제어, 레거시 제거, ObserveDailyEmotionUseCase 전환)이 구현되었습니다.
Out of Scope Changes check ✅ Passed UserRepository 및 관련 로컬/원격 데이터소스 분리, LogoutUseCase/WithdrawalUseCase의 캐시 초기화 로직 추가 등이 포함되어 있으나, 이들은 감정 데이터 흐름 리팩터링과 일관된 아키텍처 개선으로 정당화됩니다.

✏️ 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 refactor/#192-daily-emotion-flow
📝 Coding Plan
  • Generate coding plan for human review comments

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

❤️ Share

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
data/src/main/java/com/threegap/bitnagil/data/emotion/model/response/DailyEmotionResponse.kt (1)

21-28: ⚠️ Potential issue | 🟠 Major

fetchedDate는 변환 시각이 아니라 요청 기준 일자를 사용해야 합니다.

27번 줄에서 LocalDate.now()를 호출하면, EmotionRepositoryImpl.kt 46번 줄에서 고정한 요청 일자와 어긋날 수 있습니다. 자정 전후에 응답이 돌아오면 전날 데이터를 받아놓고도 fetchedDate가 오늘 날짜로 기록되어 isStalefalse가 되고, 다음 구독에서 재조회가 차단됩니다. 요청에 사용한 LocalDatetoDomain(requestedDate: LocalDate) 매개변수로 전달하는 방식이 안전합니다.

수정 예시
-fun DailyEmotionResponse.toDomain(): DailyEmotion =
+fun DailyEmotionResponse.toDomain(fetchedDate: LocalDate): DailyEmotion =
     DailyEmotion(
         type = emotionMarbleType,
         name = emotionMarbleName,
         imageUrl = imageUrl,
         homeMessage = emotionMarbleHomeMessage,
-        fetchedDate = LocalDate.now(),
+        fetchedDate = fetchedDate,
     )
-            val currentDate = LocalDate.now().toString()
-            emotionDataSource.fetchDailyEmotion(currentDate).map {
-                _dailyEmotionFlow.value = it.toDomain()
+            val requestedDate = LocalDate.now()
+            emotionDataSource.fetchDailyEmotion(requestedDate.toString()).map {
+                _dailyEmotionFlow.value = it.toDomain(requestedDate)
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@data/src/main/java/com/threegap/bitnagil/data/emotion/model/response/DailyEmotionResponse.kt`
around lines 21 - 28, DailyEmotionResponse.toDomain currently sets fetchedDate =
LocalDate.now(), which can diverge from the request date used in
EmotionRepositoryImpl and cause stale-check bugs; change the conversion to
accept a requestedDate parameter (e.g., fun
DailyEmotionResponse.toDomain(requestedDate: LocalDate)): construct DailyEmotion
with fetchedDate = requestedDate, update all callers (notably where
EmotionRepositoryImpl maps responses at the request site) to pass the same
requestedDate used for the request, and ensure the DailyEmotion data class usage
remains consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@data/src/main/java/com/threegap/bitnagil/data/emotion/repositoryImpl/EmotionRepositoryImpl.kt`:
- Around line 20-25: The current compareAndSet in isFetching causes a concurrent
fetch to short-circuit and return Result.success(Unit), which can drop a
subsequent refresh triggered by registerEmotion() or a subscription's initial
fetch; change the fetch control in fetchDailyEmotion() (the routine guarded by
isFetching.compareAndSet) so that when a fetch is already in progress it either
queues a single follow-up fetch or serializes requests: e.g., replace the
immediate success return with a flag/queue (e.g., pendingRefresh boolean) that
is set when a fetch is in progress and a new fetch is requested, and after the
in-flight fetch completes have the code check pendingRefresh and perform one
additional fetch if needed; update references to isFetching, compareAndSet,
fetchDailyEmotion(), and registerEmotion() to use this handshake so a trailing
refresh is not lost.

---

Outside diff comments:
In
`@data/src/main/java/com/threegap/bitnagil/data/emotion/model/response/DailyEmotionResponse.kt`:
- Around line 21-28: DailyEmotionResponse.toDomain currently sets fetchedDate =
LocalDate.now(), which can diverge from the request date used in
EmotionRepositoryImpl and cause stale-check bugs; change the conversion to
accept a requestedDate parameter (e.g., fun
DailyEmotionResponse.toDomain(requestedDate: LocalDate)): construct DailyEmotion
with fetchedDate = requestedDate, update all callers (notably where
EmotionRepositoryImpl maps responses at the request site) to pass the same
requestedDate used for the request, and ensure the DailyEmotion data class usage
remains consistent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 52f22984-cc82-4ed7-bda4-56d2b9b2db63

📥 Commits

Reviewing files that changed from the base of the PR and between 55c8f13 and ead9462.

📒 Files selected for processing (9)
  • data/src/main/java/com/threegap/bitnagil/data/emotion/model/response/DailyEmotionResponse.kt
  • data/src/main/java/com/threegap/bitnagil/data/emotion/repositoryImpl/EmotionRepositoryImpl.kt
  • domain/src/main/java/com/threegap/bitnagil/domain/emotion/model/DailyEmotion.kt
  • domain/src/main/java/com/threegap/bitnagil/domain/emotion/model/EmotionChangeEvent.kt
  • domain/src/main/java/com/threegap/bitnagil/domain/emotion/repository/EmotionRepository.kt
  • domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/GetEmotionChangeEventFlowUseCase.kt
  • domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/ObserveDailyEmotionUseCase.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/screen/home/HomeViewModel.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/screen/recommendroutine/RecommendRoutineViewModel.kt
💤 Files with no reviewable changes (2)
  • domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/GetEmotionChangeEventFlowUseCase.kt
  • domain/src/main/java/com/threegap/bitnagil/domain/emotion/model/EmotionChangeEvent.kt

Copy link
Contributor

@l5x5l l5x5l left a comment

Choose a reason for hiding this comment

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

Event를 각 ViewModel에서 감지해 감정 데이터 조회를 호출하는 공통 로직을 제거한 것은 좋아 보입니다! 👍
다만 수정해주신 코드에서 DailyEmotionResponse의 toDomain이나 DailyEmotion의 isStale 같이 LocalDate를 사용하는 부분을 보면 해당 정보를 인자로 전달받아 사용하기 보다는 함수 내부에서 매번 LocalData.now()를 호출하고 있어 호출 시점마다 결과가 달라질 수 있는 비결정적 코드라는 느낌을 받았습니다...!
이 부분만 외부에서 LocalDate를 주입받아 사용할 수 있도록 변경해주시면 추후 테스트시 또는 기능 유지보수시에 더 좋을 것 같아요!

@wjdrjs00
Copy link
Member Author

wjdrjs00 commented Mar 9, 2026

Event를 각 ViewModel에서 감지해 감정 데이터 조회를 호출하는 공통 로직을 제거한 것은 좋아 보입니다! 👍 다만 수정해주신 코드에서 DailyEmotionResponse의 toDomain이나 DailyEmotion의 isStale 같이 LocalDate를 사용하는 부분을 보면 해당 정보를 인자로 전달받아 사용하기 보다는 함수 내부에서 매번 LocalData.now()를 호출하고 있어 호출 시점마다 결과가 달라질 수 있는 비결정적 코드라는 느낌을 받았습니다...! 이 부분만 외부에서 LocalDate를 주입받아 사용할 수 있도록 변경해주시면 추후 테스트시 또는 기능 유지보수시에 더 좋을 것 같아요!

수정완료! -> 3edbfd1

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)
data/src/main/java/com/threegap/bitnagil/data/emotion/repositoryImpl/EmotionRepositoryImpl.kt (1)

20-25: ⚠️ Potential issue | 🟠 Major

진행 중 fetch를 성공으로 단락하면 최신 감정 갱신이 유실됩니다.

Line 44에서 이미 fetch 중이면 바로 Result.success(Unit)을 반환해서, Line 39의 후행 refresh가 Lines 23-25의 초기 구독 fetch와 겹칠 때 그대로 드롭됩니다. 첫 요청이 등록 이전 상태를 읽었다면 _dailyEmotionFlow는 같은 날짜 동안 stale로 간주되지 않아 오래된 값을 유지할 수 있습니다. 중복 호출만 막지 말고, 진행 중 요청 뒤에 refresh 1회를 보장하도록 직렬화하거나 pending 플래그를 두는 쪽이 안전합니다.

예시 수정안
-import java.util.concurrent.atomic.AtomicBoolean
+import kotlinx.coroutines.sync.Mutex

-    private val isFetching = AtomicBoolean(false)
+    private val fetchMutex = Mutex()
+    `@Volatile`
+    private var pendingRefresh = false

     override suspend fun fetchDailyEmotion(): Result<Unit> {
-        if (!isFetching.compareAndSet(false, true)) return Result.success(Unit)
+        if (!fetchMutex.tryLock()) {
+            pendingRefresh = true
+            return Result.success(Unit)
+        }
         return try {
-            val today = LocalDate.now()
-            emotionDataSource.fetchDailyEmotion(today.toString()).map {
-                _dailyEmotionFlow.value = it.toDomain(today)
-            }
+            var result: Result<Unit>
+            do {
+                pendingRefresh = false
+                val today = LocalDate.now()
+                result = emotionDataSource.fetchDailyEmotion(today.toString()).map {
+                    _dailyEmotionFlow.value = it.toDomain(today)
+                }
+            } while (pendingRefresh)
+            result
         } finally {
-            isFetching.set(false)
+            fetchMutex.unlock()
         }
     }

Also applies to: 39-52

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@data/src/main/java/com/threegap/bitnagil/data/emotion/repositoryImpl/EmotionRepositoryImpl.kt`
around lines 20 - 25, The current concurrency handling in EmotionRepositoryImpl
uses isFetching to short-circuit fetchDailyEmotion and returns early, which can
drop a later refresh requested from onSubscription and leave _dailyEmotionFlow
stale; update fetchDailyEmotion (and related callers) so that when isFetching is
true you record a pending-refresh flag (or enqueue one) instead of returning
success immediately, and when the in-flight fetch completes the code checks that
flag to run exactly one additional fetch (or emit a refresh) and clear it;
reference isFetching, fetchDailyEmotion, _dailyEmotionFlow and dailyEmotionFlow
to locate where to add the pending flag/serialization and ensure
onSubscription-triggered fetches are not lost.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@data/src/main/java/com/threegap/bitnagil/data/emotion/repositoryImpl/EmotionRepositoryImpl.kt`:
- Around line 20-25: The current concurrency handling in EmotionRepositoryImpl
uses isFetching to short-circuit fetchDailyEmotion and returns early, which can
drop a later refresh requested from onSubscription and leave _dailyEmotionFlow
stale; update fetchDailyEmotion (and related callers) so that when isFetching is
true you record a pending-refresh flag (or enqueue one) instead of returning
success immediately, and when the in-flight fetch completes the code checks that
flag to run exactly one additional fetch (or emit a refresh) and clear it;
reference isFetching, fetchDailyEmotion, _dailyEmotionFlow and dailyEmotionFlow
to locate where to add the pending flag/serialization and ensure
onSubscription-triggered fetches are not lost.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b5f7366c-1d2e-4fa6-aeff-8c044aa64d10

📥 Commits

Reviewing files that changed from the base of the PR and between ead9462 and 3edbfd1.

📒 Files selected for processing (3)
  • data/src/main/java/com/threegap/bitnagil/data/emotion/model/response/DailyEmotionResponse.kt
  • data/src/main/java/com/threegap/bitnagil/data/emotion/repositoryImpl/EmotionRepositoryImpl.kt
  • domain/src/main/java/com/threegap/bitnagil/domain/emotion/model/DailyEmotion.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

🧹 Nitpick comments (3)
presentation/src/main/java/com/threegap/bitnagil/presentation/screen/recommendroutine/RecommendRoutineViewModel.kt (1)

32-37: 실패 시에도 loadRecommendRoutines()가 호출됩니다

observeDailyEmotionUseCase()Result.failure를 emit하면 getOrNull()?.typenull을 반환하고, 이 null 값도 distinctUntilChanged()를 통과하여 loadRecommendRoutines()를 트리거합니다.

의도된 동작이라면 문제없지만, 실패 시 불필요한 API 호출을 방지하려면 성공 케이스만 필터링하는 것을 고려해 보세요.

♻️ 실패 케이스 필터링 예시
 repeatOnSubscription {
     observeDailyEmotionUseCase()
+        .filter { it.isSuccess }
         .map { result -> result.getOrNull()?.type }
         .distinctUntilChanged()
         .collect { loadRecommendRoutines() }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@presentation/src/main/java/com/threegap/bitnagil/presentation/screen/recommendroutine/RecommendRoutineViewModel.kt`
around lines 32 - 37, The current flow in repeatOnSubscription uses
observeDailyEmotionUseCase() and maps Result to result.getOrNull()?.type so a
failure becomes null and still passes through distinctUntilChanged(), causing
loadRecommendRoutines() to run on errors; update the chain to only forward
successful emissions (e.g., filter for success or mapNotNull the Result to its
type) before distinctUntilChanged() so that loadRecommendRoutines() is invoked
only when a non-null/successful emotion type is emitted from
observeDailyEmotionUseCase().
data/src/main/java/com/threegap/bitnagil/data/emotion/datasource/EmotionLocalDataSource.kt (1)

3-9: 데이터 레이어에서 도메인 모델 직접 참조

데이터 레이어(EmotionLocalDataSource)가 도메인 레이어의 DailyEmotion을 직접 import하고 있습니다. Clean Architecture 원칙상 데이터 레이어는 도메인 레이어에 의존하지 않는 것이 이상적이나, 로컬 캐시에서 별도 DTO 변환 오버헤드를 피하기 위한 실용적 선택으로 보입니다.

현재 구조가 프로젝트 전반에서 일관되게 사용된다면 문제없습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@data/src/main/java/com/threegap/bitnagil/data/emotion/datasource/EmotionLocalDataSource.kt`
around lines 3 - 9, EmotionLocalDataSource currently imports and exposes the
domain model DailyEmotion, coupling the data layer to the domain; to fix this
create a data-layer DTO/entity (e.g., DailyEmotionEntity) and change the
EmotionLocalDataSource signature to use that DTO (replace val dailyEmotion:
StateFlow<DailyEmotion?> and fun saveDailyEmotion(dailyEmotion: DailyEmotion)
with the DTO types), then implement mapping functions between DailyEmotionEntity
and com.threegap.bitnagil.domain.emotion.model.DailyEmotion in the repository or
a mapper class so conversion happens at the boundary (keep method names
saveDailyEmotion and clearCache but update their parameter/return types and add
mappers).
presentation/src/main/java/com/threegap/bitnagil/presentation/screen/home/HomeViewModel.kt (1)

59-72: 에러 핸들링이 누락되어 있습니다.

onFailure 블록이 비어 있어 에러가 무시됩니다. 이 파일의 다른 에러 핸들러(Line 265, 282)는 Log.e()로 로깅하고 있어 일관성이 떨어집니다. 디버깅을 위해 최소한 로깅을 추가하는 것이 좋습니다.

♻️ 에러 로깅 추가 제안
 private fun observeDailyEmotion() {
     intent {
         repeatOnSubscription {
             observeDailyEmotionUseCase().collect { result ->
                 result.fold(
                     onSuccess = { dailyEmotion ->
                         reduce { state.copy(dailyEmotion = dailyEmotion.toUiModel()) }
                     },
-                    onFailure = {},
+                    onFailure = { error ->
+                        Log.e("HomeViewModel", "감정 정보 가져오기 실패: ${error.message}")
+                    },
                 )
             }
         }
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@presentation/src/main/java/com/threegap/bitnagil/presentation/screen/home/HomeViewModel.kt`
around lines 59 - 72, The onFailure branch in observeDailyEmotion() currently
swallows errors; update the onFailure handler of
observeDailyEmotionUseCase().collect to log the throwable (use Log.e with a
clear tag/message similar to other handlers) and optionally update state or show
an error UI if needed; locate the onFailure lambda in observeDailyEmotion() and
replace the empty block with a Log.e(...) call referencing the throwable so
failures aren't ignored.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@data/src/main/java/com/threegap/bitnagil/data/emotion/repositoryImpl/EmotionRepositoryImpl.kt`:
- Line 22: Rename the misspelled constructor parameter and all usages from
emotionLocalDateSource to emotionLocalDataSource in EmotionRepositoryImpl:
update the primary constructor parameter name and every reference inside methods
(e.g., calls currently referencing emotionLocalDateSource) to the correct
emotionLocalDataSource identifier so the class compiles and follows proper
naming.

---

Nitpick comments:
In
`@data/src/main/java/com/threegap/bitnagil/data/emotion/datasource/EmotionLocalDataSource.kt`:
- Around line 3-9: EmotionLocalDataSource currently imports and exposes the
domain model DailyEmotion, coupling the data layer to the domain; to fix this
create a data-layer DTO/entity (e.g., DailyEmotionEntity) and change the
EmotionLocalDataSource signature to use that DTO (replace val dailyEmotion:
StateFlow<DailyEmotion?> and fun saveDailyEmotion(dailyEmotion: DailyEmotion)
with the DTO types), then implement mapping functions between DailyEmotionEntity
and com.threegap.bitnagil.domain.emotion.model.DailyEmotion in the repository or
a mapper class so conversion happens at the boundary (keep method names
saveDailyEmotion and clearCache but update their parameter/return types and add
mappers).

In
`@presentation/src/main/java/com/threegap/bitnagil/presentation/screen/home/HomeViewModel.kt`:
- Around line 59-72: The onFailure branch in observeDailyEmotion() currently
swallows errors; update the onFailure handler of
observeDailyEmotionUseCase().collect to log the throwable (use Log.e with a
clear tag/message similar to other handlers) and optionally update state or show
an error UI if needed; locate the onFailure lambda in observeDailyEmotion() and
replace the empty block with a Log.e(...) call referencing the throwable so
failures aren't ignored.

In
`@presentation/src/main/java/com/threegap/bitnagil/presentation/screen/recommendroutine/RecommendRoutineViewModel.kt`:
- Around line 32-37: The current flow in repeatOnSubscription uses
observeDailyEmotionUseCase() and maps Result to result.getOrNull()?.type so a
failure becomes null and still passes through distinctUntilChanged(), causing
loadRecommendRoutines() to run on errors; update the chain to only forward
successful emissions (e.g., filter for success or mapNotNull the Result to its
type) before distinctUntilChanged() so that loadRecommendRoutines() is invoked
only when a non-null/successful emotion type is emitted from
observeDailyEmotionUseCase().

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a32afea7-6919-4ad1-bfa5-8b2c6ecd8cd1

📥 Commits

Reviewing files that changed from the base of the PR and between 3edbfd1 and 9d4add3.

📒 Files selected for processing (10)
  • app/src/main/java/com/threegap/bitnagil/di/data/DataSourceModule.kt
  • data/src/main/java/com/threegap/bitnagil/data/emotion/datasource/EmotionLocalDataSource.kt
  • data/src/main/java/com/threegap/bitnagil/data/emotion/datasource/EmotionRemoteDataSource.kt
  • data/src/main/java/com/threegap/bitnagil/data/emotion/datasourceImpl/EmotionLocalDataSourceImpl.kt
  • data/src/main/java/com/threegap/bitnagil/data/emotion/datasourceImpl/EmotionRemoteDataSourceImpl.kt
  • data/src/main/java/com/threegap/bitnagil/data/emotion/repositoryImpl/EmotionRepositoryImpl.kt
  • domain/src/main/java/com/threegap/bitnagil/domain/emotion/repository/EmotionRepository.kt
  • domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/ObserveDailyEmotionUseCase.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/screen/home/HomeViewModel.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/screen/recommendroutine/RecommendRoutineViewModel.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • domain/src/main/java/com/threegap/bitnagil/domain/emotion/repository/EmotionRepository.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.

🧹 Nitpick comments (1)
data/src/main/java/com/threegap/bitnagil/data/emotion/repositoryImpl/EmotionRepositoryImpl.kt (1)

79-81: toDomain(today) 중복 호출 개선 필요

onSuccessmap에서 각각 toDomain(today)를 호출하여 동일한 변환이 두 번 수행됩니다. 한 번만 변환하여 재사용하는 것이 효율적입니다.

♻️ 개선 제안
-            emotionRemoteDataSource.fetchDailyEmotion(today.toString())
-                .onSuccess { emotionLocalDataSource.saveDailyEmotion(it.toDomain(today)) }
-                .map { it.toDomain(today) }
+            emotionRemoteDataSource.fetchDailyEmotion(today.toString())
+                .map { it.toDomain(today) }
+                .onSuccess { emotionLocalDataSource.saveDailyEmotion(it) }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@data/src/main/java/com/threegap/bitnagil/data/emotion/repositoryImpl/EmotionRepositoryImpl.kt`
around lines 79 - 81, The code calls toDomain(today) twice when handling
emotionRemoteDataSource.fetchDailyEmotion; refactor to compute the domain object
once, reuse it for saving and mapping. Specifically, inside the
fetchDailyEmotion chain (the call using onSuccess and map), transform the
fetched DTO to a single val domain = it.toDomain(today) (or use let/also) and
pass that same domain to emotionLocalDataSource.saveDailyEmotion(domain) and to
the downstream mapping instead of calling toDomain(today) again.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@data/src/main/java/com/threegap/bitnagil/data/emotion/repositoryImpl/EmotionRepositoryImpl.kt`:
- Around line 79-81: The code calls toDomain(today) twice when handling
emotionRemoteDataSource.fetchDailyEmotion; refactor to compute the domain object
once, reuse it for saving and mapping. Specifically, inside the
fetchDailyEmotion chain (the call using onSuccess and map), transform the
fetched DTO to a single val domain = it.toDomain(today) (or use let/also) and
pass that same domain to emotionLocalDataSource.saveDailyEmotion(domain) and to
the downstream mapping instead of calling toDomain(today) again.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 61e53630-9888-436b-bba4-caca5e28bc80

📥 Commits

Reviewing files that changed from the base of the PR and between 9d4add3 and 12f5dec.

📒 Files selected for processing (1)
  • data/src/main/java/com/threegap/bitnagil/data/emotion/repositoryImpl/EmotionRepositoryImpl.kt

Copy link
Contributor

@l5x5l l5x5l left a comment

Choose a reason for hiding this comment

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

고생하셨습니다!

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

🧹 Nitpick comments (3)
presentation/src/main/java/com/threegap/bitnagil/presentation/screen/home/HomeViewModel.kt (1)

59-72: observeDailyEmotion의 실패 처리가 누락되었습니다.

onFailure = {}로 빈 처리를 하면 감정 데이터 로드 실패 시 사용자에게 피드백이 없습니다. 에러 로깅 또는 UI 상태 반영을 고려해주세요.

♻️ 제안 수정
                         onFailure = {
+                            Log.e("HomeViewModel", "감정 데이터 가져오기 실패: ${it.message}")
                         },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@presentation/src/main/java/com/threegap/bitnagil/presentation/screen/home/HomeViewModel.kt`
around lines 59 - 72, The observeDailyEmotion() handler currently ignores
failures (onFailure = {}) so add real failure handling: capture the throwable
from observeDailyEmotionUseCase()’s Result in the onFailure lambda, log the
error (use your logger) and update ViewModel state via reduce(...) to reflect
the error (e.g., set an error message flag or clear loading in state.copy(...)
and/or set dailyEmotion to an error UI model). Use the existing symbols
observeDailyEmotion(), observeDailyEmotionUseCase(), onFailure, reduce and
state.copy(...) to locate where to insert logging and a state update so the UI
can show feedback when loading the daily emotion fails.
presentation/src/main/java/com/threegap/bitnagil/presentation/screen/mypage/MyPageViewModel.kt (1)

29-29: 실패 시 처리 로직이 누락되었습니다.

onFailure = {}로 빈 처리를 하면 네트워크 오류 시 사용자에게 아무런 피드백이 없습니다. 최소한 로그를 남기거나 에러 상태를 UI에 반영하는 것을 권장합니다.

♻️ 제안 수정
                         onFailure = {
+                            android.util.Log.e("MyPageViewModel", "유저 정보 가져오기 실패: ${it.message}")
                         },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@presentation/src/main/java/com/threegap/bitnagil/presentation/screen/mypage/MyPageViewModel.kt`
at line 29, The empty onFailure = {} in MyPageViewModel provides no feedback on
network errors; replace it with a handler that logs the throwable and surfaces
an error state to the UI (e.g., update a MutableLiveData/StateFlow or set a
field on your existing _uiState) so the UI can show an error message; locate the
network call inside MyPageViewModel where onFailure is passed and implement
logging (use your logger) and state update (e.g., _error.postValue or
_uiState.update { it.copy(error = ... ) }) in that onFailure lambda.
data/src/main/java/com/threegap/bitnagil/data/user/repositoryImpl/UserRepositoryImpl.kt (1)

52-56: toDomain()이 두 번 호출됩니다 (최적화 제안).

성공 시 onSuccess 블록에서 response.toDomain()을 호출하여 캐시에 저장하고, 이후 map { it.toDomain() }에서 다시 변환을 수행합니다. 기능상 문제는 없지만 불필요한 객체 생성이 발생합니다.

♻️ 제안 수정
             userRemoteDataSource.fetchUserProfile()
+                .map { it.toDomain() }
                 .onSuccess { response ->
-                    userLocalDataSource.saveUserProfile(response.toDomain())
+                    userLocalDataSource.saveUserProfile(response)
                 }
-                .map { it.toDomain() }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@data/src/main/java/com/threegap/bitnagil/data/user/repositoryImpl/UserRepositoryImpl.kt`
around lines 52 - 56, The code calls response.toDomain() twice (once in
onSuccess and again in map); fix by converting to domain once and reusing it: in
the chain, replace the separate onSuccess + map with a single transformation
that does val domain = response.toDomain();
userLocalDataSource.saveUserProfile(domain); return domain — i.e., inside the
mapping lambda (for userRemoteDataSource.fetchUserProfile()), compute domain
once, call userLocalDataSource.saveUserProfile(domain), and emit domain,
removing the redundant toDomain() call in onSuccess/map.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@data/src/main/java/com/threegap/bitnagil/data/user/datasource/UserLocalDataSource.kt`:
- Line 8: Remove the unnecessary suspend modifier from
UserLocalDataSource.saveUserProfile: the implementation only calls
_userProfile.update { userProfile } (no suspend points), so change the signature
to a regular function to match EmotionLocalDataSource.saveDailyEmotion() and
clearCache() and maintain API consistency; update any call sites if they treat
it as suspend (remove coroutine/suspend usage) and ensure the method remains
non-blocking.

In
`@presentation/src/main/java/com/threegap/bitnagil/presentation/screen/home/HomeViewModel.kt`:
- Around line 257-274: The observeUserProfile() handler increments loadingCount
once at subscription but decrements it on every emission, which can drive
loadingCount negative; modify observeUserProfile() so it only decrements
loadingCount once (e.g., on the first successful or failed emission) by tracking
a local flag or using an operator that emits only the first emission for the
decrement logic; ensure observeUserProfileUseCase() collections still update
userNickname on every emission but only the first emission triggers reduce {
state.copy(loadingCount = state.loadingCount - 1) } while subsequent emissions
skip the decrement.

In
`@presentation/src/main/java/com/threegap/bitnagil/presentation/screen/mypage/MyPageViewModel.kt`:
- Line 27: reduce 블록에서 state.copy(name = it.nickname, profileUrl =
"profileUrl")처럼 profileUrl을 하드코딩하고 있으므로 UserProfile/데이터 소스에서 실제 값을 사용하도록 수정하세요:
확인할 대상은 UserProfile 모델(필드명: profileUrl)과 reduce 내부에서 전달되는 객체(it)의 프로파일 URL
필드명(예: it.profileUrl 또는 it.avatarUrl 등); 가능한 fixes는 UserProfile에 profileUrl 필드
추가 후 state.copy(name = it.nickname, profileUrl = it.profileUrl)로 변경하거나, 실제 프로필
URL을 반환하는 다른 소스에서 값을 받아 state.copy에 전달하거나 당장이라면 임시로 빈 문자열 state.copy(name =
it.nickname, profileUrl = "")로 변경하는 것입니다.

---

Nitpick comments:
In
`@data/src/main/java/com/threegap/bitnagil/data/user/repositoryImpl/UserRepositoryImpl.kt`:
- Around line 52-56: The code calls response.toDomain() twice (once in onSuccess
and again in map); fix by converting to domain once and reusing it: in the
chain, replace the separate onSuccess + map with a single transformation that
does val domain = response.toDomain();
userLocalDataSource.saveUserProfile(domain); return domain — i.e., inside the
mapping lambda (for userRemoteDataSource.fetchUserProfile()), compute domain
once, call userLocalDataSource.saveUserProfile(domain), and emit domain,
removing the redundant toDomain() call in onSuccess/map.

In
`@presentation/src/main/java/com/threegap/bitnagil/presentation/screen/home/HomeViewModel.kt`:
- Around line 59-72: The observeDailyEmotion() handler currently ignores
failures (onFailure = {}) so add real failure handling: capture the throwable
from observeDailyEmotionUseCase()’s Result in the onFailure lambda, log the
error (use your logger) and update ViewModel state via reduce(...) to reflect
the error (e.g., set an error message flag or clear loading in state.copy(...)
and/or set dailyEmotion to an error UI model). Use the existing symbols
observeDailyEmotion(), observeDailyEmotionUseCase(), onFailure, reduce and
state.copy(...) to locate where to insert logging and a state update so the UI
can show feedback when loading the daily emotion fails.

In
`@presentation/src/main/java/com/threegap/bitnagil/presentation/screen/mypage/MyPageViewModel.kt`:
- Line 29: The empty onFailure = {} in MyPageViewModel provides no feedback on
network errors; replace it with a handler that logs the throwable and surfaces
an error state to the UI (e.g., update a MutableLiveData/StateFlow or set a
field on your existing _uiState) so the UI can show an error message; locate the
network call inside MyPageViewModel where onFailure is passed and implement
logging (use your logger) and state update (e.g., _error.postValue or
_uiState.update { it.copy(error = ... ) }) in that onFailure lambda.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a584fe50-b268-4049-9992-47835f28f2cc

📥 Commits

Reviewing files that changed from the base of the PR and between 12f5dec and b9e2e88.

📒 Files selected for processing (16)
  • app/src/main/java/com/threegap/bitnagil/di/data/DataSourceModule.kt
  • data/build.gradle.kts
  • data/src/main/java/com/threegap/bitnagil/data/user/datasource/UserLocalDataSource.kt
  • data/src/main/java/com/threegap/bitnagil/data/user/datasource/UserRemoteDataSource.kt
  • data/src/main/java/com/threegap/bitnagil/data/user/datasourceImpl/UserLocalDataSourceImpl.kt
  • data/src/main/java/com/threegap/bitnagil/data/user/datasourceImpl/UserRemoteDataSourceImpl.kt
  • data/src/main/java/com/threegap/bitnagil/data/user/repositoryImpl/UserRepositoryImpl.kt
  • data/src/test/java/com/threegap/bitnagil/data/user/repositoryImpl/UserRepositoryImplTest.kt
  • domain/src/main/java/com/threegap/bitnagil/domain/auth/usecase/LogoutUseCase.kt
  • domain/src/main/java/com/threegap/bitnagil/domain/auth/usecase/WithdrawalUseCase.kt
  • domain/src/main/java/com/threegap/bitnagil/domain/user/repository/UserRepository.kt
  • domain/src/main/java/com/threegap/bitnagil/domain/user/usecase/GetUserProfileUseCase.kt
  • domain/src/main/java/com/threegap/bitnagil/domain/user/usecase/ObserveUserProfileUseCase.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/screen/home/HomeViewModel.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/screen/mypage/MyPageViewModel.kt
  • presentation/src/main/java/com/threegap/bitnagil/presentation/screen/onboarding/OnBoardingViewModel.kt

@wjdrjs00 wjdrjs00 merged commit d289f4a into develop Mar 15, 2026
2 checks passed
@wjdrjs00 wjdrjs00 deleted the refactor/#192-daily-emotion-flow branch March 15, 2026 10:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🔨 Refactor 기존 기능 개선 🧤 대현

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[REFACTOR] DailyEmotion 데이터 흐름을 StateFlow 기반 반응형 스트림으로 개선

2 participants