Skip to content

[Feat] 활동 기록 API 서버 연동 - #93

Merged
choijungp merged 4 commits into
developfrom
feat/activityHistoryAPI
Jul 28, 2026
Merged

[Feat] 활동 기록 API 서버 연동#93
choijungp merged 4 commits into
developfrom
feat/activityHistoryAPI

Conversation

@choijungp

@choijungp choijungp commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

🌁 Background

활동 기록 관련 API 로직을 구현했어요 ~

  • 월별 뱃지 조회
  • 감정 구슬 기록 조회

📱 Screenshot

iPhone 16 Pro
Simulator Screenshot - iPhone 16 Pro - 2026-07-22 at 21 00 55
  • 가장 상단 Label은 서버 응답 값 수정 (줄바꿈 추가) 예정입니다.

👩‍💻 Contents

  • 활동 기록 Repository 구현 (ActivityHistoryRepository)
  • 서버 응답값에 맞춘 UI 구현부 일부 수정

📝 Review Note

1. MarbleEmotionMarble

enum Marble:String, CaseIterable {
    case NONE
    case CALM
    case VITALITY
    case LETHARGY
    case ANXIETY
    case SATISFACTION
    case FATIGUE
    // ...
}

struct EmotionMarble {
    let marble: Marble
    let imageUrl: String
}

기존 Marble enum이 있지만 서버에서 받은 응답 값인 imageUrl을 저장하기 위해 EmotionMarble 구조체를 따로 만들었습니다.

2. 감정 구슬 기록 캐싱

ActivityHistoryViewModel에서 monthKey(yyyy-MM)를 기준으로 월별 감정 기록을 캐싱했습니다.
사용자가 같은 달을 다시 조회하거나 달력을 이동할 때 이미 조회한 데이터라면 API를 재호출하지 않고 캐시된 데이터를 사용하도록 하여 불필요한 네트워크 요청을 줄였습니다.

monthKey가 yyyy-MM 형식으로 이루어져 있지만, yyyy년 MM월에 해당하는 감정 구슬 기록 조회 요청을 할 때
MM 이전 달의 6일, MM 이후 달의 6일을 추가적으로 더 불러오도록 하였습니다.

Summary by CodeRabbit

  • 새로운 기능
    • 활동 기록 화면에서 월간 배지와 감정 히스토리를 서버에서 조회해 표시합니다.
    • 달력에서 월을 변경하거나 화면에 진입하면 해당 기간의 데이터가 자동으로 갱신됩니다.
    • 감정 이미지와 배지 이미지를 원격 콘텐츠로 불러와 보여줍니다.
  • 개선
    • 감정 히스토리를 월 단위로 캐시해 재조회 시 더 빠르게 로딩합니다.
    • 감정 상세 화면에서 감정 설명과 이미지가 선택한 항목 기준으로 정확히 표시됩니다.

@choijungp
choijungp requested a review from taipaise July 22, 2026 12:09
@choijungp choijungp self-assigned this Jul 22, 2026
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8c04055a-12c9-44b6-aef2-b806ee0b714b

📥 Commits

Reviewing files that changed from the base of the PR and between 551067e and 70acd5f.

📒 Files selected for processing (1)
  • Projects/Presentation/Sources/ActivityHistory/View/Component/ActivityBadgeExpertComponentView.swift

Walkthrough

활동 기록 화면이 mock 데이터 대신 서버의 월간 배지와 감정 기록을 사용하도록 변경되었습니다. 도메인 모델, API 저장소, DI 주입, 월별 캐시, URL 이미지 로딩 및 달력·상세 화면 표시 흐름이 추가되었습니다.

Changes

활동 기록 데이터 흐름

Layer / File(s) Summary
도메인 계약과 응답 변환
Projects/Domain/Sources/Entity/*, Projects/Domain/Sources/Protocol/Repository/*, Projects/DataSource/Sources/DTO/*
배지·감정 기록 엔터티와 저장소 프로토콜을 추가하고 API 응답 DTO를 도메인 엔터티로 변환합니다.
활동 기록 네트워크 저장소
Projects/DataSource/Sources/Endpoint/ActivityHistoryEndpoint.swift, Projects/DataSource/Sources/Repository/ActivityHistoryRepository.swift, Projects/DataSource/Sources/Common/DataSourceDependencyAssembler.swift
월간 배지와 감정 기록 API 요청, DTO 디코딩, 오류 매핑 및 저장소 DI 등록을 구현합니다.
캐시와 화면 데이터 오케스트레이션
Projects/Presentation/Sources/ActivityHistory/ViewModel/ActivityHistoryViewModel.swift, Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift
ViewModel이 저장소를 호출하고 감정 기록을 월별로 캐시하며 저장소 의존성을 주입받도록 변경합니다.
활동 기록 화면 모델과 렌더링
Projects/Presentation/Sources/ActivityHistory/*, Projects/Presentation/Sources/Common/DesignSystem/BitnagilGraphic.swift, Projects/Presentation/Sources/EmotionRegister/Model/Emotion.swift
배지·감정 모델을 서버 이미지 URL 기반으로 변경하고 달력·상세 화면에서 새 모델과 이미지 로딩 흐름을 사용합니다.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ActivityHistoryViewController
  participant ActivityHistoryViewModel
  participant ActivityHistoryRepository
  participant NetworkService
  ActivityHistoryViewController->>ActivityHistoryViewModel: 활동 기록 입력 전달
  ActivityHistoryViewModel->>ActivityHistoryRepository: 월간 배지 또는 감정 기록 조회
  ActivityHistoryRepository->>NetworkService: ActivityHistoryEndpoint 요청
  NetworkService-->>ActivityHistoryRepository: 디코딩된 DTO 반환
  ActivityHistoryRepository-->>ActivityHistoryViewModel: 도메인 엔터티 반환
  ActivityHistoryViewModel-->>ActivityHistoryViewController: 화면 데이터 발행
Loading

Possibly related PRs

Suggested reviewers: taipaise

Poem

토끼가 API 문을 두드려요
배지와 감정 구슬이 뛰어나와요
달력은 새 기록을 품고
이미지는 URL을 타고 반짝
캐시는 달을 포근히 저장해요 🐰

🚥 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 활동 기록 API 서버 연동이라는 핵심 변경을 간결하게 잘 요약하고 있어 변경 내용과 일치합니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/activityHistoryAPI

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@Projects/Presentation/Sources/ActivityHistory/ViewModel/ActivityHistoryViewModel.swift`:
- Line 33: ActivityHistoryViewModel의 networkRetryHandler가 생성만 되고 fetch 실패 처리에
연결되지 않았습니다. fetchMonthlyBadges와 fetchEmotionMarbles의 각 catch 블록에서
DomainError.requireRetry를 구분해 networkRetryHandler를 통해 재시도하고, 재시도 불가능한 오류와 최종 실패는
기존 로깅 및 상태 처리로 유지하세요.
- Line 27: Update the unstructured Task blocks in fetchMonthlyBadge and
fetchEmotionHistory to run with MainActor isolation, including all
emotionHistoryCache access and CurrentValueSubject.send() calls consumed by the
UI. Keep the existing Task-based flow and behavior unchanged while ensuring both
methods’ asynchronous UI updates and dictionary reads/writes execute on the main
actor.
- Around line 93-100: Update the loop building emotionHistoryRecords so an
unrecognized emotionMarbleType skips only that individual emotionHistory record,
rather than returning from the surrounding Task closure. Preserve processing of
all valid records and allow the subsequent emotionHistoryCache update and
monthlyEmotionHistorySubject.send call to execute.
- Around line 44-49: Update action(input:) for .invalidateCacheAndRefetch so
EmotionCalendarView.currentPage is synchronized with the refresh: reset the
calendar page to .now before fetching, or use the current page as the date for
fetchMonthlyBadge and fetchEmotionHistory. Ensure the displayed month and
refreshed data always match when the reused view reappears.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3ab98bef-5e62-441a-a8c5-fae258edfba8

📥 Commits

Reviewing files that changed from the base of the PR and between 0b9598d and 551067e.

⛔ Files ignored due to path filters (39)
  • Projects/Presentation/Resources/Images.xcassets/Badge/emotion_multiple_badge.imageset/emotion_multiple_badge.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Badge/emotion_multiple_badge.imageset/emotion_multiple_badge@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Badge/emotion_multiple_badge.imageset/emotion_multiple_badge@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Badge/emotion_single_badge.imageset/emotion_single_badge.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Badge/emotion_single_badge.imageset/emotion_single_badge@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Badge/emotion_single_badge.imageset/emotion_single_badge@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Badge/none_badge.imageset/none_badge.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Badge/none_badge.imageset/none_badge@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Badge/none_badge.imageset/none_badge@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Badge/report_multiple_badge.imageset/report_multiple_badge.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Badge/report_multiple_badge.imageset/report_multiple_badge@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Badge/report_multiple_badge.imageset/report_multiple_badge@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Badge/report_single_badge.imageset/report_single_badge.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Badge/report_single_badge.imageset/report_single_badge@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Badge/report_single_badge.imageset/report_single_badge@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Badge/routine_multiple_badge.imageset/routine_multiple_badge.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Badge/routine_multiple_badge.imageset/routine_multiple_badge@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Badge/routine_multiple_badge.imageset/routine_multiple_badge@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Badge/routine_single_badge.imageset/routine_single_badge.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Badge/routine_single_badge.imageset/routine_single_badge@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Badge/routine_single_badge.imageset/routine_single_badge@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/EmotionActivity/anxiety_emotion_graphic.imageset/anxiety_emotion_graphic.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/EmotionActivity/anxiety_emotion_graphic.imageset/anxiety_emotion_graphic@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/EmotionActivity/anxiety_emotion_graphic.imageset/anxiety_emotion_graphic@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/EmotionActivity/calm_emotion_graphic.imageset/calm_emotion_graphic.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/EmotionActivity/calm_emotion_graphic.imageset/calm_emotion_graphic@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/EmotionActivity/calm_emotion_graphic.imageset/calm_emotion_graphic@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/EmotionActivity/fatigue_emotion_graphic.imageset/fatigue_emotion_graphic.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/EmotionActivity/fatigue_emotion_graphic.imageset/fatigue_emotion_graphic@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/EmotionActivity/fatigue_emotion_graphic.imageset/fatigue_emotion_graphic@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/EmotionActivity/lethargy_emotion_graphic.imageset/lethargy_emotion_graphic.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/EmotionActivity/lethargy_emotion_graphic.imageset/lethargy_emotion_graphic@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/EmotionActivity/lethargy_emotion_graphic.imageset/lethargy_emotion_graphic@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/EmotionActivity/satisfaction_emotion_graphic.imageset/satisfaction_emotion_graphic.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/EmotionActivity/satisfaction_emotion_graphic.imageset/satisfaction_emotion_graphic@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/EmotionActivity/satisfaction_emotion_graphic.imageset/satisfaction_emotion_graphic@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/EmotionActivity/vitality_emotion_graphic.imageset/vitality_emotion_graphic.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/EmotionActivity/vitality_emotion_graphic.imageset/vitality_emotion_graphic@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/EmotionActivity/vitality_emotion_graphic.imageset/vitality_emotion_graphic@3x.png is excluded by !**/*.png
📒 Files selected for processing (32)
  • Projects/DataSource/Sources/Common/DataSourceDependencyAssembler.swift
  • Projects/DataSource/Sources/DTO/EmotionMarbleHistoryDTO.swift
  • Projects/DataSource/Sources/DTO/MonthlyBadgesDTO.swift
  • Projects/DataSource/Sources/Endpoint/ActivityHistoryEndpoint.swift
  • Projects/DataSource/Sources/Repository/ActivityHistoryRepository.swift
  • Projects/Domain/Sources/Entity/BadgeEntity.swift
  • Projects/Domain/Sources/Entity/EmotionHistoryEntity.swift
  • Projects/Domain/Sources/Protocol/Repository/ActivityHistoryRepositoryProtocol.swift
  • Projects/Presentation/Resources/Images.xcassets/Badge/emotion_multiple_badge.imageset/Contents.json
  • Projects/Presentation/Resources/Images.xcassets/Badge/emotion_single_badge.imageset/Contents.json
  • Projects/Presentation/Resources/Images.xcassets/Badge/none_badge.imageset/Contents.json
  • Projects/Presentation/Resources/Images.xcassets/Badge/report_multiple_badge.imageset/Contents.json
  • Projects/Presentation/Resources/Images.xcassets/Badge/report_single_badge.imageset/Contents.json
  • Projects/Presentation/Resources/Images.xcassets/Badge/routine_multiple_badge.imageset/Contents.json
  • Projects/Presentation/Resources/Images.xcassets/Badge/routine_single_badge.imageset/Contents.json
  • Projects/Presentation/Resources/Images.xcassets/EmotionActivity/Contents.json
  • Projects/Presentation/Resources/Images.xcassets/EmotionActivity/anxiety_emotion_graphic.imageset/Contents.json
  • Projects/Presentation/Resources/Images.xcassets/EmotionActivity/calm_emotion_graphic.imageset/Contents.json
  • Projects/Presentation/Resources/Images.xcassets/EmotionActivity/fatigue_emotion_graphic.imageset/Contents.json
  • Projects/Presentation/Resources/Images.xcassets/EmotionActivity/lethargy_emotion_graphic.imageset/Contents.json
  • Projects/Presentation/Resources/Images.xcassets/EmotionActivity/satisfaction_emotion_graphic.imageset/Contents.json
  • Projects/Presentation/Resources/Images.xcassets/EmotionActivity/vitality_emotion_graphic.imageset/Contents.json
  • Projects/Presentation/Sources/ActivityHistory/Model/ActivityBadge.swift
  • Projects/Presentation/Sources/ActivityHistory/Model/EmotionMarble.swift
  • Projects/Presentation/Sources/ActivityHistory/View/ActivityHistoryViewController.swift
  • Projects/Presentation/Sources/ActivityHistory/View/Component/ActivityBadgeSectionView.swift
  • Projects/Presentation/Sources/ActivityHistory/View/Component/EmotionCalendarView.swift
  • Projects/Presentation/Sources/ActivityHistory/View/EmotionDetailViewController.swift
  • Projects/Presentation/Sources/ActivityHistory/ViewModel/ActivityHistoryViewModel.swift
  • Projects/Presentation/Sources/Common/DesignSystem/BitnagilGraphic.swift
  • Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift
  • Projects/Presentation/Sources/EmotionRegister/Model/Emotion.swift
💤 Files with no reviewable changes (16)
  • Projects/Presentation/Resources/Images.xcassets/EmotionActivity/calm_emotion_graphic.imageset/Contents.json
  • Projects/Presentation/Resources/Images.xcassets/Badge/emotion_single_badge.imageset/Contents.json
  • Projects/Presentation/Resources/Images.xcassets/Badge/routine_multiple_badge.imageset/Contents.json
  • Projects/Presentation/Resources/Images.xcassets/EmotionActivity/lethargy_emotion_graphic.imageset/Contents.json
  • Projects/Presentation/Resources/Images.xcassets/EmotionActivity/vitality_emotion_graphic.imageset/Contents.json
  • Projects/Presentation/Resources/Images.xcassets/Badge/none_badge.imageset/Contents.json
  • Projects/Presentation/Resources/Images.xcassets/EmotionActivity/anxiety_emotion_graphic.imageset/Contents.json
  • Projects/Presentation/Resources/Images.xcassets/EmotionActivity/Contents.json
  • Projects/Presentation/Resources/Images.xcassets/Badge/routine_single_badge.imageset/Contents.json
  • Projects/Presentation/Resources/Images.xcassets/EmotionActivity/fatigue_emotion_graphic.imageset/Contents.json
  • Projects/Presentation/Resources/Images.xcassets/Badge/report_multiple_badge.imageset/Contents.json
  • Projects/Presentation/Resources/Images.xcassets/Badge/emotion_multiple_badge.imageset/Contents.json
  • Projects/Presentation/Resources/Images.xcassets/EmotionActivity/satisfaction_emotion_graphic.imageset/Contents.json
  • Projects/Presentation/Sources/EmotionRegister/Model/Emotion.swift
  • Projects/Presentation/Resources/Images.xcassets/Badge/report_single_badge.imageset/Contents.json
  • Projects/Presentation/Sources/Common/DesignSystem/BitnagilGraphic.swift


init() {
private let calendar = Calendar.current
private var emotionHistoryCache: [String: [String: EmotionMarble]] = [:]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift

emotionHistoryCache에 대한 동시성 레이스 컨디션 위험.

fetchMonthlyBadgefetchEmotionHistory가 각각 별도의 비구조적 Task {}로 실행되고, fetchEmotionHistory는 그 안에서 emotionHistoryCache(일반 Dictionary, 스레드 안전하지 않음)를 읽고 씁니다. 달력을 빠르게 넘기면 여러 Task가 동시에 실행되어 같은 딕셔너리에 동시 접근할 수 있어 실제 데이터 레이스가 발생할 수 있습니다. 또한 응답이 요청 순서와 다르게 도착하면(느린 이전 달 응답이 나중에 도착) 최신 페이지의 배지/감정 데이터가 과거 응답으로 덮어써질 수 있습니다.

동일 파일의 이전 PR에서 subject를 send()하는 Task {}Task { @mainactor in ... }로 감싸라는 학습 내용이 있었습니다. 이 패턴을 적용하면 emotionHistoryCache 접근이 메인 액터로 격리되어 실제 데이터 레이스는 방지되며(순서 역전으로 인한 논리적 stale 표시는 별도 이슈로 남습니다).

🔒 제안: MainActor로 격리
     private func fetchEmotionHistory(date: Date) {
         let key = monthKey(for: date)

         if let cachedEmotionHistory = emotionHistoryCache[key] {
             monthlyEmotionHistorySubject.send(cachedEmotionHistory)
             return
         }

-        Task {
+        Task { `@MainActor` in
             do {

fetchMonthlyBadge에도 동일하게 적용해 주세요.

Based on learnings, in this file's Task{}/CurrentValueSubject.send() pattern consumed by UI sinks, "prefer keeping the Task {} wrapper but change it to Task { MainActor in ... }".

Also applies to: 59-108

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Projects/Presentation/Sources/ActivityHistory/ViewModel/ActivityHistoryViewModel.swift`
at line 27, Update the unstructured Task blocks in fetchMonthlyBadge and
fetchEmotionHistory to run with MainActor isolation, including all
emotionHistoryCache access and CurrentValueSubject.send() calls consumed by the
UI. Keep the existing Task-based flow and behavior unchanged while ensuring both
methods’ asynchronous UI updates and dictionary reads/writes execute on the main
actor.

Source: Learnings

private let monthlyEmotionHistorySubject = CurrentValueSubject<[String: EmotionMarble], Never>([:])

private let activityHistoryRepository: ActivityHistoryRepositoryProtocol
private let networkRetryHandler: NetworkRetryHandler

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

networkRetryHandler가 생성만 되고 재시도 로직에 전혀 사용되지 않습니다.

activityHistoryRepository는 재시도 가능한 오류를 DomainError.requireRetry로 던지는데(ActivityHistoryRepository.fetchMonthlyBadges/fetchEmotionMarbles 참고), 두 catch 블록 모두 에러 종류를 구분하지 않고 로깅만 하고 끝냅니다. networkRetryHandler가 인스턴스화된 걸 보면 재시도 처리가 의도되었던 것으로 보이나 실제로는 연결되어 있지 않아, 일시적 네트워크 오류 시 사용자가 재시도 없이 빈 화면을 보게 됩니다.

Also applies to: 37-37, 66-68, 104-106

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Projects/Presentation/Sources/ActivityHistory/ViewModel/ActivityHistoryViewModel.swift`
at line 33, ActivityHistoryViewModel의 networkRetryHandler가 생성만 되고 fetch 실패 처리에
연결되지 않았습니다. fetchMonthlyBadges와 fetchEmotionMarbles의 각 catch 블록에서
DomainError.requireRetry를 구분해 networkRetryHandler를 통해 재시도하고, 재시도 불가능한 오류와 최종 실패는
기존 로깅 및 상태 처리로 유지하세요.

Comment on lines 44 to +49
func action(input: Input) {
switch input {
case .invalidateCacheAndRefetch:
emotionHistoryCache.removeAll()
fetchMonthlyBadge(date: .now)
fetchEmotionHistory(date: .now)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the relevant files
fd -a 'ActivityHistoryViewModel.swift|ActivityHistoryViewController.swift' Projects || true

# Show outlines first
ast-grep outline Projects/Presentation/Sources/ActivityHistory/ViewModel/ActivityHistoryViewModel.swift --view expanded || true
ast-grep outline Projects/Presentation/Sources/ActivityHistory/ActivityHistoryViewController.swift --view expanded || true

# Read the relevant ranges with line numbers
sed -n '1,220p' Projects/Presentation/Sources/ActivityHistory/ViewModel/ActivityHistoryViewModel.swift | cat -n
printf '\n==== CONTROLLER ====\n'
sed -n '1,320p' Projects/Presentation/Sources/ActivityHistory/ActivityHistoryViewController.swift | cat -n

Repository: YAPP-Github/Bitnagil-iOS

Length of output: 6340


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the correct controller file and any lifecycle/calendar logic
ast-grep outline Projects/Presentation/Sources/ActivityHistory/View/ActivityHistoryViewController.swift --view expanded || true
sed -n '1,360p' Projects/Presentation/Sources/ActivityHistory/View/ActivityHistoryViewController.swift | cat -n

printf '\n==== SEARCH FOR RELEVANT CALLS ====\n'
rg -n "invalidateCacheAndRefetch|viewWillAppear|FSCalendar|setCurrentPage|currentPage|selectedDate|tabBar" Projects/Presentation/Sources/ActivityHistory Projects/Presentation/Sources -g '!**/Generated/**' || true

Repository: YAPP-Github/Bitnagil-iOS

Length of output: 33798


🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline Projects/Presentation/Sources/ActivityHistory/View/Component/EmotionCalendarView.swift --view expanded || true
sed -n '1,280p' Projects/Presentation/Sources/ActivityHistory/View/Component/EmotionCalendarView.swift | cat -n

printf '\n==== TAB BAR / PERSISTENCE CONTEXT ====\n'
sed -n '1,140p' Projects/Presentation/Sources/Common/View/TabBarView.swift | cat -n

Repository: YAPP-Github/Bitnagil-iOS

Length of output: 14516


캘린더 페이지를 현재 월과 동기화해야 합니다. TabBarView에서 ActivityHistoryViewController가 재사용되는데, viewWillAppear마다 캐시를 지우고 .now 기준으로만 다시 불러오면서 EmotionCalendarViewcalendar.currentPage는 유지됩니다. 다른 달을 보고 나갔다가 돌아오면 화면의 월과 데이터가 어긋날 수 있으니, 갱신 시 현재 페이지를 .now로 리셋하거나 현재 페이지 기준으로 다시 조회하도록 맞춰야 합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Projects/Presentation/Sources/ActivityHistory/ViewModel/ActivityHistoryViewModel.swift`
around lines 44 - 49, Update action(input:) for .invalidateCacheAndRefetch so
EmotionCalendarView.currentPage is synchronized with the refresh: reset the
calendar page to .now before fetching, or use the current page as the date for
fetchMonthlyBadge and fetchEmotionHistory. Ensure the displayed month and
refreshed data always match when the reused view reappears.

Comment on lines +93 to +100
var emotionHistoryRecords: [String: EmotionMarble] = [:]
for emotionHistory in emotionHistoryEntities {
guard let marble = Marble(rawValue: emotionHistory.emotionMarbleType)
else { return }

let emotionMarble = EmotionMarble(marble: marble, imageUrl: emotionHistory.imageUrl)
emotionHistoryRecords[emotionHistory.date] = emotionMarble
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

알 수 없는 emotionMarbleType 하나가 전체 달의 감정 기록 조회를 무효화합니다.

for 루프 안의 guard ... else { return }는 루프 반복을 건너뛰는 게 아니라 Task 클로저 전체를 종료시킵니다. 서버가 클라이언트가 아직 모르는 emotionMarbleType을 하나라도 반환하면, 이미 처리된 나머지 레코드까지 모두 버려지고 캐시 저장(emotionHistoryCache[key] = ...)과 monthlyEmotionHistorySubject.send(...)가 전부 스킵됩니다. 결과적으로 해당 월의 캘린더가 통째로 비어 보이게 됩니다.

🐛 제안: 개별 레코드만 스킵
                 var emotionHistoryRecords: [String: EmotionMarble] = [:]
                 for emotionHistory in emotionHistoryEntities {
                     guard let marble = Marble(rawValue: emotionHistory.emotionMarbleType)
-                    else { return }
+                    else {
+                        BitnagilLogger.log(logType: .error, message: "알 수 없는 emotionMarbleType: \(emotionHistory.emotionMarbleType)")
+                        continue
+                    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var emotionHistoryRecords: [String: EmotionMarble] = [:]
for emotionHistory in emotionHistoryEntities {
guard let marble = Marble(rawValue: emotionHistory.emotionMarbleType)
else { return }
let emotionMarble = EmotionMarble(marble: marble, imageUrl: emotionHistory.imageUrl)
emotionHistoryRecords[emotionHistory.date] = emotionMarble
}
var emotionHistoryRecords: [String: EmotionMarble] = [:]
for emotionHistory in emotionHistoryEntities {
guard let marble = Marble(rawValue: emotionHistory.emotionMarbleType)
else {
BitnagilLogger.log(logType: .error, message: "알 수 없는 emotionMarbleType: \(emotionHistory.emotionMarbleType)")
continue
}
let emotionMarble = EmotionMarble(marble: marble, imageUrl: emotionHistory.imageUrl)
emotionHistoryRecords[emotionHistory.date] = emotionMarble
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Projects/Presentation/Sources/ActivityHistory/ViewModel/ActivityHistoryViewModel.swift`
around lines 93 - 100, Update the loop building emotionHistoryRecords so an
unrecognized emotionMarbleType skips only that individual emotionHistory record,
rather than returning from the surrounding Task closure. Preserve processing of
all valid records and allow the subsequent emotionHistoryCache update and
monthlyEmotionHistorySubject.send call to execute.

@taipaise taipaise left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

캐싱까지 구현하시다니 좋와요~~ 고생하셨습니다!!

@choijungp
choijungp merged commit 2d72a75 into develop Jul 28, 2026
2 checks passed
@choijungp
choijungp deleted the feat/activityHistoryAPI branch July 28, 2026 14:31
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.

2 participants