Skip to content

[QA-58] 타임 캡슐이 모두 삭제된 경우에도 일일리포트가 존재하는 경우 확인 가능 - #278

Merged
sunjoolee merged 9 commits into
developfrom
QA-58
Mar 5, 2026
Merged

sunjoolee merged 9 commits into
developfrom
QA-58

Conversation

@harry7408

@harry7408 harry7408 commented Mar 2, 2026 •

Copy link
Copy Markdown
Contributor

Related Issue

작업 상세 내용

  • 타임 캡슐 + 일일리포트 존재 여부 API를 호출하여 일일리포트 확인이 가능하도록 수정

Summary by CodeRabbit

  • 새 기능

    • 캘린더에 타임캡슐 또는 데일리 리포트가 있는 날짜 표시 기능 추가
    • 바텀시트가 로드 결과를 앱 상태로 알리는 콜백 지원 추가
  • 개선 사항

    • 선택 로직 개선: 날짜 선택 시 콘텐츠 유무에 따라 열기/처리 로직 강화
    • 월별 콘텐츠 날짜 로딩 및 캐싱 도입으로 성능·응답성 향상
    • 빈 날짜에 대한 사용자 안내 메시지 추가

@harry7408 harry7408 linked an issue Mar 2, 2026 that may be closed by this pull request
1 task
@coderabbitai

coderabbitai Bot commented Mar 2, 2026 •

Copy link
Copy Markdown
Contributor

Walkthrough

원격 API부터 DI, 데이터/도메인 레이어, ViewModel 및 UI까지 캘린더 날짜 조회 기능이 추가되었습니다. 원격 호출로 년/월 단위 날짜 목록을 받아 로컬에서 파싱·캐싱하고 UI에 반영하도록 흐름을 연결합니다.

Changes

Cohort / File(s) Summary
원격 계층 - API 및 응답 모델
core/remote/src/main/java/com/emotionstorage/remote/api/CalendarApiService.kt, core/remote/src/main/java/com/emotionstorage/remote/response/calendar/CalendarDatesResponse.kt
Retrofit 엔드포인트 GET api/v1/calendar/date 추가 및 응답 DTO(CalendarDatesResponse) 정의.
원격 계층 - 데이터 소스 구현
core/remote/src/main/java/com/emotionstorage/remote/dataSourceImpl/CalendarRemoteDataSourceImpl.kt
API 호출을 래핑하는 원격 데이터 소스 구현 추가; 응답의 dates를 반환(널이면 빈 리스트).
원격 계층 - DI
core/remote/src/main/java/com/emotionstorage/remote/di/ApiServiceModule.kt, core/remote/src/main/java/com/emotionstorage/remote/di/RemoteDataSourceModule.kt
CalendarApiService 제공자와 CalendarRemoteDataSource 바인딩 추가.
데이터 계층 - 인터페이스 및 구현
data/src/main/java/com/emotionstorage/data/dataSource/remote/CalendarRemoteDataSource.kt, data/src/main/java/com/emotionstorage/data/repoImpl/CalendarRepositoryImpl.kt, data/src/main/java/com/emotionstorage/data/di/RepositoryModule.kt
원격 데이터 소스 인터페이스 추가, 리포지토리 구현은 문자열 날짜들을 안전하게 LocalDate로 파싱해 Set으로 반환하고 DI에 바인딩.
도메인 계층 - 리포지토리/유스케이스
domain/src/main/java/com/emotionstorage/domain/repo/CalendarRepository.kt, domain/src/main/java/com/emotionstorage/domain/useCase/calendar/GetCalendarContentDatesUseCase.kt
CalendarRepository 인터페이스와 해당 유스케이스(Add) 추가.
프레젠테이션 - ViewModel 변경
feat/time-capsule/src/main/java/com/emotionstorage/time_capsule/presentation/CalendarViewModel.kt
GetCalendarContentDatesUseCase 주입, 월별 content 날짜 캐싱(Mutex 보호)과 state(calendarContentDates, isDailyReportChecked) 추가 및 날짜 선택/로딩 흐름 확장.
프레젠테이션 - UI 컴포넌트
feat/time-capsule/src/main/java/com/emotionstorage/time_capsule/ui/CalendarScreen.kt, feat/time-capsule/src/main/java/com/emotionstorage/time_capsule/ui/component/TimeCapsuleBottomSheet.kt
BottomSheet에 onTimeCapsulesLoaded 콜백 추가, UI에서 타임캡슐 날짜와 콘텐츠 날짜를 합쳐 렌더링하도록 변경 및 빈 상태 메시지 추가.

Sequence Diagram

sequenceDiagram
    participant User
    participant CalendarScreen
    participant CalendarViewModel
    participant GetCalendarContentDatesUseCase
    participant CalendarRepository
    participant CalendarApiService
    participant Backend

    User->>CalendarScreen: 년/월 선택
    CalendarScreen->>CalendarViewModel: selectYearMonth(yearMonth)
    CalendarViewModel->>CalendarViewModel: ensureMonthContentDatesCached(yearMonth)
    CalendarViewModel->>GetCalendarContentDatesUseCase: invoke(year, month)
    GetCalendarContentDatesUseCase->>CalendarRepository: getTimeCapsuleOrDailyReportExistDate(year, month)
    CalendarRepository->>CalendarApiService: getTimeCapsuleOrDailyReportExistDate(year, month)
    CalendarApiService->>Backend: GET /api/v1/calendar/date?year={year}&month={month}
    Backend-->>CalendarApiService: CalendarDatesResponse
    CalendarApiService-->>CalendarRepository: ResponseDto<CalendarDatesResponse>
    CalendarRepository->>CalendarRepository: dates -> LocalDate 파싱
    CalendarRepository-->>GetCalendarContentDatesUseCase: Set<LocalDate>
    GetCalendarContentDatesUseCase-->>CalendarViewModel: Set<LocalDate>
    CalendarViewModel->>CalendarViewModel: 캐시에 저장 및 state 업데이트
    CalendarViewModel-->>CalendarScreen: state 반영
    CalendarScreen->>User: 캘린더 렌더링 (타임캡슐 + 콘텐츠 날짜)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • sunjoolee
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 주요 변경 사항(타임 캡슐 삭제 후에도 일일리포트 확인 가능)을 명확하고 구체적으로 설명하며, 제출된 변경 사항과 완벽하게 일치합니다.
Description check ✅ Passed PR 설명은 이슈 번호와 작업 상세 내용을 포함하여 템플릿 요구사항을 충족하고 있습니다.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch QA-58

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
feat/time-capsule/src/main/java/com/emotionstorage/time_capsule/presentation/CalendarViewModel.kt (1)

253-263: 일일리포트 조회가 동일 선택에서 중복 호출될 수 있습니다.

TimeCapsulesLoaded 처리에서 Line [254], Line [262]의 setDailyReportState(date)가 추가되면서, 같은 날짜 선택 흐름에서 중복 조회가 발생할 수 있습니다. 조회 책임을 한 경로로 정리하면 불필요한 네트워크 호출과 로그 노이즈를 줄일 수 있습니다.

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

In
`@feat/time-capsule/src/main/java/com/emotionstorage/time_capsule/presentation/CalendarViewModel.kt`
around lines 253 - 263, The TimeCapsulesLoaded handler calls
setDailyReportState(date) from two paths causing duplicate fetches; consolidate
so setDailyReportState(date) is invoked only once per selection—either remove
the second call inside the hasDailyReport branch or add a guard before calling
(e.g., check isDailyReportChecked or an isFetching flag) so
setDailyReportState(date) is executed only when not already requested; update
the TimeCapsulesLoaded flow around the itemCount/hasDailyReport logic and the
reduce { state.copy(isDailyReportChecked = true) } usage to ensure a single
responsibility for triggering setDailyReportState.
🤖 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/remote/src/main/java/com/emotionstorage/remote/dataSourceImpl/CalendarRemoteDataSourceImpl.kt`:
- Around line 14-15: The method calling
calendarApiService.getTimeCapsuleOrDailyReportExistDate currently returns
emptyList() when response.data is null which hides errors; change this to raise
an error so upstream can handle failures: in the method that calls
calendarApiService.getTimeCapsuleOrDailyReportExistDate (the function in
CalendarRemoteDataSourceImpl that uses response.data), check if response.data is
null and throw a descriptive exception (e.g., IllegalStateException or a domain
RemoteDataException) including context (API name and parameters) instead of
returning emptyList(), so consumers can distinguish "no dates" from
"invalid/failed response".

In
`@data/src/main/java/com/emotionstorage/data/repoImpl/CalendarRepositoryImpl.kt`:
- Line 17: 현재 CalendarRepositoryImpl의 날짜 파싱(날짜 리스트를 처리하는 부분: dates.mapNotNull {
runCatching { LocalDate.parse(it) }.getOrNull() }.toSet())에서 실패한 항목을 무시하고 있어 누락
원인 추적이 어렵습니다; 수정 방법은 해당 블록에서 runCatching의 onFailure를 사용하거나 파싱 결과를 분리하여 실패한 문자열들을
수집한 뒤(예: failedDates 리스트 또는 실패 카운터) CalendarRepositoryImpl의 로거 또는 모니터링 이벤트로 실패
개수와 실패한 원시 값(또는 샘플)을 기록하도록 변경하세요; 이렇게 하면 LocalDate.parse 호출의 실패 사례와 개수를 남겨
운영/디버깅 시 누락 원인 추적이 가능해집니다.

In
`@feat/time-capsule/src/main/java/com/emotionstorage/time_capsule/presentation/CalendarViewModel.kt`:
- Around line 282-291: 현재 getCalendarContentDates 호출 실패 시 runCatching.getOrElse
{ emptySet() }로 대체한 결과를 무조건 monthContentDatesCache에 저장하고 있어 일시적 오류로 영구적으로 빈 데이터가
캐시됩니다; 수정 방법은 실패 시 캐시에 쓰지 않고 성공했을 때만 캐시에 저장하도록 변경하는 것입니다 — 예를 들어 runCatching {
getCalendarContentDates(...) } 결과를 검사해 isSuccess일 때만 cacheMutex.withLock {
monthContentDatesCache[yearMonth] = result }로 갱신하거나 onSuccess 블록을 사용하여 성공 결과만
캐시에 넣도록 변경하세요; 관련 심볼: getCalendarContentDates, monthContentDatesCache,
cacheMutex, yearMonth.

In
`@feat/time-capsule/src/main/java/com/emotionstorage/time_capsule/ui/component/TimeCapsuleBottomSheet.kt`:
- Around line 71-74: Remove itemCount from the LaunchedEffect keys and introduce
a local hasReported flag to ensure the onTimeCapsulesLoaded callback runs only
once after initial load: change LaunchedEffect(date, isNotLoading, itemCount) to
LaunchedEffect(date, isNotLoading) and add a mutable hasReported = remember {
mutableStateOf(false) } (or rememberSaveable as needed); inside the effect, only
call onTimeCapsulesLoaded(date, itemCount) when isNotLoading &&
!hasReported.value, then set hasReported.value = true after invoking it; this
prevents repeated dispatches during paging while preserving the guard in
handleTimeCapsulesLoaded and keeping state.isDailyReportChecked logic intact.

---

Nitpick comments:
In
`@feat/time-capsule/src/main/java/com/emotionstorage/time_capsule/presentation/CalendarViewModel.kt`:
- Around line 253-263: The TimeCapsulesLoaded handler calls
setDailyReportState(date) from two paths causing duplicate fetches; consolidate
so setDailyReportState(date) is invoked only once per selection—either remove
the second call inside the hasDailyReport branch or add a guard before calling
(e.g., check isDailyReportChecked or an isFetching flag) so
setDailyReportState(date) is executed only when not already requested; update
the TimeCapsulesLoaded flow around the itemCount/hasDailyReport logic and the
reduce { state.copy(isDailyReportChecked = true) } usage to ensure a single
responsibility for triggering setDailyReportState.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 423a274 and ad3c53e.

📒 Files selected for processing (13)
  • core/remote/src/main/java/com/emotionstorage/remote/api/CalendarApiService.kt
  • core/remote/src/main/java/com/emotionstorage/remote/dataSourceImpl/CalendarRemoteDataSourceImpl.kt
  • core/remote/src/main/java/com/emotionstorage/remote/di/ApiServiceModule.kt
  • core/remote/src/main/java/com/emotionstorage/remote/di/RemoteDataSourceModule.kt
  • core/remote/src/main/java/com/emotionstorage/remote/response/calendar/CalendarDatesResponse.kt
  • data/src/main/java/com/emotionstorage/data/dataSource/remote/CalendarRemoteDataSource.kt
  • data/src/main/java/com/emotionstorage/data/di/RepositoryModule.kt
  • data/src/main/java/com/emotionstorage/data/repoImpl/CalendarRepositoryImpl.kt
  • domain/src/main/java/com/emotionstorage/domain/repo/CalendarRepository.kt
  • domain/src/main/java/com/emotionstorage/domain/useCase/calendar/GetCalendarContentDatesUseCase.kt
  • feat/time-capsule/src/main/java/com/emotionstorage/time_capsule/presentation/CalendarViewModel.kt
  • feat/time-capsule/src/main/java/com/emotionstorage/time_capsule/ui/CalendarScreen.kt
  • feat/time-capsule/src/main/java/com/emotionstorage/time_capsule/ui/component/TimeCapsuleBottomSheet.kt

@Emotion-Storage Emotion-Storage deleted a comment from coderabbitai Bot Mar 2, 2026
@Emotion-Storage Emotion-Storage deleted a comment from coderabbitai Bot Mar 2, 2026
@harry7408 harry7408 self-assigned this Mar 2, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 (2)
feat/time-capsule/src/main/java/com/emotionstorage/time_capsule/presentation/CalendarViewModel.kt (2)

83-84: 월 캐시 무효화 경로가 없어 최신 상태 반영이 지연될 수 있어요

Line 83-84의 캐시는 Line 279에서 한 번 적재되면 계속 재사용됩니다. 같은 달에서 타임캡슐 삭제/일일리포트 생성 후에도 이전 데이터가 남을 수 있으니, 최소한 월 재진입 시 강제 갱신 옵션을 두는 편이 안전합니다.

제안 수정안
-            val contentDates = ensureMonthContentDatesCached(yearMonth)
+            val contentDates = ensureMonthContentDatesCached(
+                yearMonth = yearMonth,
+                forceRefresh = true,
+            )
-    private suspend fun ensureMonthContentDatesCached(yearMonth: YearMonth): Set<LocalDate> {
+    private suspend fun ensureMonthContentDatesCached(
+        yearMonth: YearMonth,
+        forceRefresh: Boolean = false,
+    ): Set<LocalDate> {
         Logger.d("ensureMonthContentDatesCached: $yearMonth")
         cacheMutex.withLock {
-            monthContentDatesCache[yearMonth]?.let { return it }
+            if (!forceRefresh) {
+                monthContentDatesCache[yearMonth]?.let { return it }
+            }
         }

Also applies to: 164-165, 276-280

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

In
`@feat/time-capsule/src/main/java/com/emotionstorage/time_capsule/presentation/CalendarViewModel.kt`
around lines 83 - 84, monthContentDatesCache is never invalidated so month data
can become stale after creating/deleting items; update the code to invalidate or
refresh the cache when state changes. Specifically: add a clear/invalidate
method (e.g., invalidateMonthCache or invalidate(YearMonth)) that uses
cacheMutex to synchronously remove the affected YearMonth entry from
monthContentDatesCache, call it from mutation handlers (e.g., after time-capsule
delete, after daily-report create, and on month re-entry/refresh paths), and
optionally add a boolean forceRefresh parameter to the existing loader (the
function that reads from monthContentDatesCache) to bypass cache when needed.
Ensure all places that modify month content call this invalidator so the cache
stays current.

195-216: 동일 날짜에서 일일리포트 조회가 중복 호출될 수 있습니다

현재 Line 213에서 setDailyReportState(date)를 먼저 호출하고, 이후 Line 254/262에서도 다시 호출됩니다. 네트워크 중복 호출과 상태 깜빡임 가능성이 있어 조회 지점을 하나로 정리하는 게 좋습니다.

제안 수정안
 private fun handleSelectCalendarDate(date: LocalDate) =
     baseIntent {
         val canOpen =
             (date in state.calendarTimeCapsuleDates) || (date in state.calendarContentDates)

         if (!canOpen) return@baseIntent

         reduce {
             state.copy(
                 calendarSelectedDate = date,
                 timeCapsulesFlow = null,
                 dailyReportId = null,
                 isNewDailyReport = false,
                 isDailyReportChecked = false,
             )
         }

         setTimeCapsulesFlow(date)
-        setDailyReportState(date)

         postSideEffect(CalendarSideEffect.ShowTimeCapsuleBottomSheet)
     }
 private fun handleTimeCapsulesLoaded(
     date: LocalDate,
     itemCount: Int,
 ) = baseIntent {
     if (state.calendarSelectedDate != date) return@baseIntent
     if (state.isDailyReportChecked) return@baseIntent
+    reduce { state.copy(isDailyReportChecked = true) }

     if (itemCount > 0) {
         setDailyReportState(date)
-        reduce { state.copy(isDailyReportChecked = true) }
     } else {
         val ym = YearMonth.from(date)
         val monthDates = ensureMonthContentDatesCached(ym)
         val hasDailyReport = monthDates.contains(date)

         if (hasDailyReport) {
             setDailyReportState(date)
         } else {
             reduce {
                 state.copy(
                     dailyReportId = null,
                     isNewDailyReport = false,
                 )
             }
         }
-
-        reduce { state.copy(isDailyReportChecked = true) }
     }
 }

Also applies to: 253-263

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

In
`@feat/time-capsule/src/main/java/com/emotionstorage/time_capsule/presentation/CalendarViewModel.kt`
around lines 195 - 216, handleSelectCalendarDate currently calls
setDailyReportState(date) but the same function is invoked again elsewhere
causing duplicate network calls and UI flicker; consolidate the daily report
fetch so it's invoked only once by removing the extra setDailyReportState calls
from other call sites (or replace them with a no-op guard) and keep the single
call inside handleSelectCalendarDate after reduce and setTimeCapsulesFlow;
alternatively make setDailyReportState(idempotent by checking if
state.calendarSelectedDate == date or isDailyReportChecked/isLoading is true and
returning early) so duplicate invocations don't trigger extra network requests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@feat/time-capsule/src/main/java/com/emotionstorage/time_capsule/presentation/CalendarViewModel.kt`:
- Around line 282-290: The code currently converts a failure from
getCalendarContentDates into emptySet() (via fetchedResult.getOrNull() ?: return
emptySet()), which conflates an actual empty-month result with an error and
causes canOpen logic to incorrectly block access; instead detect and propagate
errors separately: change the handling around
runCatching/getCalendarContentDates so that failures are not mapped to
emptySet—e.g., check fetchedResult.isFailure (or use
fetchedResult.getOrThrow()/fold) and return or surface a distinct error/null
result for CalendarViewModel (symbols to update: getCalendarContentDates,
fetchedResult, fetched and the canOpen branch) so the canOpen logic can
differentiate an error from a true empty set and behave correctly.

---

Nitpick comments:
In
`@feat/time-capsule/src/main/java/com/emotionstorage/time_capsule/presentation/CalendarViewModel.kt`:
- Around line 83-84: monthContentDatesCache is never invalidated so month data
can become stale after creating/deleting items; update the code to invalidate or
refresh the cache when state changes. Specifically: add a clear/invalidate
method (e.g., invalidateMonthCache or invalidate(YearMonth)) that uses
cacheMutex to synchronously remove the affected YearMonth entry from
monthContentDatesCache, call it from mutation handlers (e.g., after time-capsule
delete, after daily-report create, and on month re-entry/refresh paths), and
optionally add a boolean forceRefresh parameter to the existing loader (the
function that reads from monthContentDatesCache) to bypass cache when needed.
Ensure all places that modify month content call this invalidator so the cache
stays current.
- Around line 195-216: handleSelectCalendarDate currently calls
setDailyReportState(date) but the same function is invoked again elsewhere
causing duplicate network calls and UI flicker; consolidate the daily report
fetch so it's invoked only once by removing the extra setDailyReportState calls
from other call sites (or replace them with a no-op guard) and keep the single
call inside handleSelectCalendarDate after reduce and setTimeCapsulesFlow;
alternatively make setDailyReportState(idempotent by checking if
state.calendarSelectedDate == date or isDailyReportChecked/isLoading is true and
returning early) so duplicate invocations don't trigger extra network requests.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ad3c53e and 58a1fb1.

📒 Files selected for processing (1)
  • feat/time-capsule/src/main/java/com/emotionstorage/time_capsule/presentation/CalendarViewModel.kt

@harry7408
harry7408 requested a review from sunjoolee March 2, 2026 14:07

@sunjoolee sunjoolee left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

고생하셨습니다!! :)

@sunjoolee
sunjoolee merged commit 10e4252 into develop Mar 5, 2026
3 checks passed
@sunjoolee
sunjoolee deleted the QA-58 branch March 5, 2026 03:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] 타임캡슐 컨텐츠가 없는 상태의 Bottom Sheet (QA-58)

2 participants