Skip to content

Conversation

@baekteun
Copy link
Member

@baekteun baekteun commented Nov 26, 2025

💡 개요

Meal과 TimeTable 결과를 save하여 cache-first로 동작하도록 구성

Summary by CodeRabbit

릴리스 노트

  • New Features

    • 주간 보기가 기본으로 전환되어 항상 주 단위 UI 흐름을 사용합니다.
    • 식단 및 시간표에 대한 로컬 영구 캐시(테이블 추가) 도입으로 오프라인 접근성과 응답성 향상.
    • 캐시 기반 반환 시 백그라운드 동기화로 데이터를 최신 상태로 유지합니다.
    • 식단 엔티티에 비어있는지 확인하는 isEmpty 속성 추가.
  • Bug Fixes

    • 주간 토글 관련 불필요한 분기 제거로 UI 동작 일관성 개선.

✏️ Tip: You can customize this high-level summary in your review settings.

@github-actions
Copy link
Contributor

🛠️ 이슈와 PR의 Labels 동기화를 스킵했어요.

@github-actions
Copy link
Contributor

✅ PR의 Assign 자동 지정을 성공했어요!

@baekteun

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 26, 2025

Caution

Review failed

The pull request is closed.

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

주간 모드가 기본값(.weekly)으로 전환되고 일간 관련 분기 및 레거시 코어(mealCore, timeTableCore)가 제거되었습니다. 식단 및 시간표는 로컬 DB 엔티티로 직렬화·저장되며, 클라이언트는 캐시 우선 반환 후 백그라운드에서 서버와 동기화합니다.

Changes

Cohort / File(s) Summary
메인 통합: 주간 모드 기본화
Projects/Feature/MainFeature/Sources/MainCore.swift, Projects/Feature/MainFeature/Sources/MainView.swift
State의 dateSelectionMode 기본값을 .daily.weekly로 변경. Action에서 mealCore/timeTableCore/weeklyModeUpdated 제거. 리듀서와 서브피쳐 연결을 weeklyMealCore/weeklyTimeTableCore로 단순화하고 일간/주간 분기 제거.
주간 코어 단순화
Projects/Feature/MealFeature/Sources/Weekly/WeeklyMealCore.swift
onLoad에서 초기 로컬 로드·병합 흐름 제거. displayDate 변경 감시만으로 refreshData를 발생시키도록 간소화.
로컬 엔티티 도입/확장
Projects/Shared/Entity/Sources/Local/MealLocalEntity.swift, Projects/Shared/Entity/Sources/Local/TimeTableLocalEntity.swift, Projects/Shared/Entity/Sources/Meal.swift
MealLocalEntity를 단일 date: Date → 여러 필드(id, date(String), 각 식사 JSON 문자열/칼로리, createdAt)로 변경 및 변환 초기자/역직렬화 메서드 추가. TimeTableLocalEntity 신규 추가(일별 JSON 저장/복원). MealisEmpty 계산 프로퍼티 추가.
로컬 DB 클라이언트 확장 및 마이그레이션
Projects/Shared/LocalDatabaseClient/Sources/LocalDatabaseClient.swift
컬럼 기반 조회 유틸 (readRecordByColumn, readRecordsByColumn) 추가. 마이그레이션 v1.2.0으로 mealLocalEntitytimeTableLocalEntity 테이블 생성 스크립트 추가.
클라이언트 의존성 변경
Projects/Shared/MealClient/Project.swift, Projects/Shared/TimeTableClient/Project.swift
MealClientTimeTableClient 타깃에 .shared(target: .LocalDatabaseClient) 의존성 추가.
클라이언트 캐싱 및 동기화 로직
Projects/Shared/MealClient/Sources/MealClient.swift, Projects/Shared/TimeTableClient/Sources/TimeTableClient.swift
로컬 DB 우선 캐시 전략 도입(날짜 포맷 유틸 포함). 캐시 히트 시 즉시 반환하고 백그라운드 Task로 서버 동기화 수행, 미스 시 서버에서 가져와 로컬에 저장 후 반환하도록 fetch/sync 헬퍼 추가. 공개 API 시그니처는 변경 없음.
버전 업데이트
Projects/App/iOS/Support/Info.plist
CFBundleShortVersionString 11.3→12.0, CFBundleVersion 84→85로 갱신.

Sequence Diagram(s)

sequenceDiagram
    participant UI as MainView / User
    participant Core as WeeklyMealCore / WeeklyTimeTableCore
    participant Client as MealClient / TimeTableClient
    participant DB as LocalDatabaseClient
    participant API as NeisAPI

    Note over UI,Core: 사용자가 날짜(또는 displayDate) 변경
    UI->>Core: refreshData 액션
    Core->>Client: fetch(date)
    Client->>DB: readRecordByColumn(record:, column:"date", value: key)
    alt 캐시 히트
        DB-->>Client: cachedRecord
        Client-->>Core: cached Domain Model
        Client->>API: Task (background) syncFromServer(date)
        API-->>DB: upsert 최신 데이터
    else 캐시 미스
        Client->>API: fetchFromServer(date)
        API-->>Client: serverData
        Client->>DB: upsert per-date entity
        DB-->>Client: 저장 완료
        Client-->>Core: server Domain Model
    end
    Core-->>UI: 화면 갱신
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

주의 집중 영역:

  • Projects/Shared/MealClient/Sources/MealClient.swift, Projects/Shared/TimeTableClient/Sources/TimeTableClient.swift: 캐시 우선 로직, 배경 동기화(Task) 경쟁 상태, 에러/비어있는 데이터 처리.
  • Projects/Shared/Entity/Sources/Local/MealLocalEntity.swift, Projects/Shared/Entity/Sources/Local/TimeTableLocalEntity.swift: JSON 직렬화/역직렬화 및 날짜 포맷 무결성.
  • Projects/Shared/LocalDatabaseClient/Sources/LocalDatabaseClient.swift: v1.2.0 마이그레이션의 제약/인덱스와 동작 안전성.
  • Projects/Feature/MainFeature/*: 분기 제거로 인한 사이드 이펙트(analytics, 접근성 레이블 등) 누락 여부 확인.

Possibly related PRs

Suggested labels

✨ Feature

Poem

🐇 한 주를 껴안고 폴짝,
날짜는 문자열로 정리했지요,
캐시 먼저 건네고 살짝 뒤에서 동기화,
당근처럼 쫄깃한 데이터, 냠🥕
토끼가 축하해요, 훌륭해요!

Pre-merge checks and finishing touches

❌ 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%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 풀 리퀘스트 제목이 주요 변경 사항을 명확하게 설명합니다. Meal과 TimeTable 데이터를 캐싱하여 cache-first 방식으로 동작하도록 변경하는 것이 주요 목표이며, 이는 변경 사항 요약에 반영되어 있습니다.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b2b6a9c and afc6585.

📒 Files selected for processing (1)
  • Projects/Shared/Entity/Sources/Local/TimeTableLocalEntity.swift (1 hunks)

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.

@gemini-code-assist
Copy link

Summary of Changes

Hello @baekteun, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

이 PR은 Meal 및 TimeTable 데이터 로딩 방식을 캐시 우선(cache-first) 전략으로 전환하여 사용자 경험을 개선하고 오프라인 접근성을 높이는 것을 목표로 합니다. 이를 위해 로컬 데이터베이스에 식단 및 시간표 데이터를 저장하고 관리하는 새로운 엔티티와 로직이 도입되었습니다. 또한, 기존의 일별/주별 뷰 분리 로직을 주별 뷰로 통합하여 코드 복잡성을 줄이고 일관된 사용자 인터페이스를 제공합니다.

Highlights

  • 캐시 우선 로직 도입: Meal 및 TimeTable 데이터 로딩에 캐시 우선(cache-first) 전략을 적용하여 로컬 데이터베이스에서 먼저 데이터를 조회하고, 없으면 서버에서 가져온 후 캐시하는 방식으로 변경했습니다.
  • 로컬 데이터베이스 스키마 추가: MealLocalEntityTimeTableLocalEntity를 정의하고, GRDB 마이그레이션을 통해 관련 데이터베이스 테이블을 생성하여 데이터를 로컬에 저장할 수 있도록 했습니다.
  • 일별/주별 뷰 통합 및 간소화: MainCoreMainView에서 기존의 일별(daily) 식단/시간표 관련 로직과 UI를 제거하고, 주별(weekly) 뷰로 통합하여 코드 복잡성을 줄이고 일관된 사용자 경험을 제공합니다. dateSelectionMode의 기본값도 .weekly로 변경되었습니다.
  • Meal 엔티티 확장: Meal 구조체에 isEmpty 계산 속성을 추가하여 식단 데이터가 비어있는지 여부를 쉽게 확인할 수 있도록 했습니다.
  • LocalDatabaseClient 기능 확장: readRecordByColumnreadRecordsByColumn 메서드를 추가하여 특정 컬럼을 기준으로 로컬 데이터베이스 레코드를 조회할 수 있는 기능을 제공합니다.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

이 PR은 급식 및 시간표 데이터를 로컬 데이터베이스에 캐시하여 'cache-first' 전략을 구현하는 변경 사항을 담고 있습니다. 이를 통해 앱 성능과 사용자 경험이 개선될 것으로 기대됩니다. 전반적인 구현 방향은 훌륭하지만, 몇 가지 개선할 점이 보입니다. 특히 데이터베이스 상호작용의 효율성, 캐시 로직의 정확성, 그리고 코드 일관성 측면에서 몇 가지 피드백을 드립니다. 자세한 내용은 각 파일의 주석을 참고해주세요.

Comment on lines 203 to 204
try? localDatabaseClient.delete(record: TimeTableLocalEntity.self, key: entity.id)
try? localDatabaseClient.save(record: entity)

Choose a reason for hiding this comment

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

medium

syncTimeTableFromServer 함수에서 delete를 호출하고 save를 하고 있습니다. TimeTableLocalEntityid는 새로 생성된 UUID이므로 delete는 아무런 효과가 없습니다. timeTableLocalEntity 테이블의 date 컬럼에 unique(onConflict: .replace) 제약 조건이 있으므로 save만 호출해도 데이터가 올바르게 갱신됩니다. 불필요한 delete 호출을 제거하는 것이 좋습니다.

    try? localDatabaseClient.save(record: entity)

Copy link
Contributor

@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

Caution

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

⚠️ Outside diff range comments (1)
Projects/Feature/MealFeature/Sources/Weekly/WeeklyMealCore.swift (1)

71-74: 빈 catch 블록은 에러를 숨깁니다.

알레르기 정보 로딩 실패 시 에러가 무시되고 있습니다. 최소한 로깅을 추가하거나, 사용자에게 기본 동작을 명확히 하는 것이 좋습니다.

 do {
     state.allergyList = try localDatabaseClient.readRecords(as: AllergyLocalEntity.self)
         .compactMap { AllergyType(rawValue: $0.allergy) ?? nil }
-} catch {}
+} catch {
+    // 알레르기 정보 로딩 실패 시 빈 목록으로 진행
+    state.allergyList = []
+}
🧹 Nitpick comments (8)
Projects/Shared/Entity/Sources/Meal.swift (1)

38-40: LGTM! 다만 중복 코드 제거를 권장합니다.

새로운 isEmpty 속성이 올바르게 구현되었습니다. WeeklyMealCore.State.DayMeal.isEmpty (lines 27-31)에서 동일한 로직이 중복되어 있으니, 해당 코드를 이 속성을 활용하도록 리팩토링하면 좋겠습니다.

WeeklyMealCore.swiftDayMeal.isEmpty를 다음과 같이 단순화할 수 있습니다:

public var isEmpty: Bool {
    meal.isEmpty
}
Projects/Feature/MealFeature/Sources/Weekly/WeeklyMealCore.swift (1)

64-68: 구독이 중복 등록될 수 있습니다.

onLoad가 여러 번 호출되면 $displayDate.publisher 구독이 중복으로 등록될 수 있습니다. 취소 ID를 추가하여 기존 구독을 취소하거나, 한 번만 호출되도록 보장하는 것이 좋습니다.

+    private enum CancellableID: Hashable {
+        case fetch
+        case displayDateSubscription
+    }
 case .onLoad:
-    return .publisher {
+    return .cancel(id: CancellableID.displayDateSubscription)
+        .merge(with: .publisher {
         state.$displayDate.publisher
             .map { _ in Action.refreshData }
-    }
+    }.cancellable(id: CancellableID.displayDateSubscription))
Projects/Feature/MainFeature/Sources/MainView.swift (1)

113-118: normalizedWeekStart 계산이 중복됩니다.

previousWeek, currentWeekStart, nextWeek는 이미 startOfWeek 또는 previousWeekStart/nextWeekStart를 통해 계산되었으므로, Line 117에서 다시 startOfWeek(for: weekStart)를 호출하는 것은 불필요합니다.

-                        ForEach([previousWeek, currentWeekStart, nextWeek], id: \.timeIntervalSince1970) { weekStart in
-                            let normalizedWeekStart = datePolicy.startOfWeek(for: weekStart)
+                        ForEach([previousWeek, currentWeekStart, nextWeek], id: \.timeIntervalSince1970) { normalizedWeekStart in
Projects/Shared/Entity/Sources/Local/TimeTableLocalEntity.swift (2)

22-24: persistenceKey 메서드가 사용되지 않음

persistenceKey(for:) 메서드가 정의되어 있지만, 클라이언트 코드에서는 date 컬럼을 직접 사용하여 조회하고 있습니다. 관련 코드 스니펫에서 MealLocalEntity.swift도 동일한 패턴을 가지고 있어 일관성은 있으나, 사용되지 않는 코드로 보입니다. 향후 사용 계획이 있다면 유지하고, 그렇지 않다면 제거를 고려해 주세요.


49-68: DateFormatter 인스턴스 재사용 권장

toTimeTables() 메서드가 호출될 때마다 새로운 DateFormatter 인스턴스를 생성합니다. DateFormatter는 생성 비용이 높으므로, 정적 프로퍼티로 캐싱하는 것을 권장합니다.

+private extension TimeTableLocalEntity {
+    static let dateFormatter: DateFormatter = {
+        let formatter = DateFormatter()
+        formatter.dateFormat = "yyyyMMdd"
+        formatter.timeZone = .autoupdatingCurrent
+        formatter.locale = .autoupdatingCurrent
+        return formatter
+    }()
+}
+
 public extension TimeTableLocalEntity {
     // ...
     
     func toTimeTables() -> [TimeTable] {
         let decoder = JSONDecoder()
         guard let items = try? decoder.decode([TimeTableItem].self, from: Data(timeTableData.utf8)) else {
             return []
         }
 
-        let dateFormatter = DateFormatter()
-        dateFormatter.dateFormat = "yyyyMMdd"
-        dateFormatter.timeZone = .autoupdatingCurrent
-        dateFormatter.locale = .autoupdatingCurrent
-        let dateObject = dateFormatter.date(from: date)
+        let dateObject = Self.dateFormatter.date(from: date)
 
         return items.map { item in
             // ...
         }
     }
 }
Projects/Shared/TimeTableClient/Sources/TimeTableClient.swift (1)

16-20: formatDate 구현이 MealClient와 다릅니다

TimeTableClient에서는 수동 문자열 조합을, MealClient에서는 DateFormatter를 사용합니다. 일관성과 유지보수를 위해 공통 유틸리티로 통합하거나 동일한 방식을 사용하는 것을 권장합니다. 관련 코드 스니펫(Projects/Shared/MealClient/Sources/MealClient.swift, Lines 14-19)을 참고하세요.

Projects/Feature/MainFeature/Sources/MainCore.swift (2)

38-69: 주간 모드 기본값으로 바뀌면서 dateSelectionMode가 사실상 상수가 되었습니다

리듀서 내에서 더 이상 dateSelectionMode를 바꾸는 액션이 없어 항상 .weekly로만 동작합니다. 일별 모드를 완전히 제거한 것이라면 DateSelectionMode.daily.daily 분기, 관련 로직들을 정리해 두면 상태 정의가 더 단순해질 것 같습니다.


161-168: .refresh 분기에서 DatePolicy를 생성만 하고 사용하지 않아 사실상 no-op입니다

weeklyMealCore(.refresh) / weeklyTimeTableCore(.refresh) 케이스에서 isSkipWeekend, isSkipAfterDinner, datePolicy를 계산하지만 상태 변경이나 이펙트가 없어 동작에 영향을 주지 않습니다.
refresh 시점에 더 이상 날짜 보정이 필요 없다면 이 케이스를 통째로 제거하고, 여전히 DatePolicy 기반 보정이 필요하다면 displayDate 갱신 등 실제 동작으로 연결해 두는 편이 명확할 것 같습니다. 기존 요구사항과 비교해 한 번만 확인 부탁드립니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0e006d7 and 04cb921.

📒 Files selected for processing (11)
  • Projects/Feature/MainFeature/Sources/MainCore.swift (4 hunks)
  • Projects/Feature/MainFeature/Sources/MainView.swift (2 hunks)
  • Projects/Feature/MealFeature/Sources/Weekly/WeeklyMealCore.swift (1 hunks)
  • Projects/Shared/Entity/Sources/Local/MealLocalEntity.swift (1 hunks)
  • Projects/Shared/Entity/Sources/Local/TimeTableLocalEntity.swift (1 hunks)
  • Projects/Shared/Entity/Sources/Meal.swift (1 hunks)
  • Projects/Shared/LocalDatabaseClient/Sources/LocalDatabaseClient.swift (2 hunks)
  • Projects/Shared/MealClient/Project.swift (1 hunks)
  • Projects/Shared/MealClient/Sources/MealClient.swift (4 hunks)
  • Projects/Shared/TimeTableClient/Project.swift (1 hunks)
  • Projects/Shared/TimeTableClient/Sources/TimeTableClient.swift (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
Projects/Shared/Entity/Sources/Meal.swift (1)
Projects/App/macOS-Widget/Sources/MealWidgetEntryView.swift (1)
  • meals (48-59)
Projects/Feature/MainFeature/Sources/MainView.swift (3)
Projects/Feature/MainFeature/Sources/DatePolicy.swift (4)
  • startOfWeek (106-112)
  • previousWeekStart (119-122)
  • nextWeekStart (124-127)
  • weekDisplayText (35-56)
Projects/Shared/TWLog/Sources/TWLog.swift (1)
  • event (63-68)
Projects/UserInterface/DesignSystem/Sources/TWFont/Font+tw.swift (2)
  • twFont (4-11)
  • twFont (13-18)
Projects/Shared/TimeTableClient/Sources/TimeTableClient.swift (7)
Projects/Shared/MealClient/Sources/MealClient.swift (1)
  • formatDate (15-20)
Projects/Shared/LocalDatabaseClient/Sources/LocalDatabaseClient.swift (6)
  • readRecordByColumn (43-51)
  • save (11-15)
  • save (17-23)
  • readRecordsByColumn (53-61)
  • delete (77-83)
  • delete (85-92)
Projects/Shared/Entity/Sources/Local/TimeTableLocalEntity.swift (1)
  • toTimeTables (49-68)
Projects/Shared/NeisClient/Sources/NeisClient.swift (1)
  • fetchDataOnNeis (8-32)
Projects/Shared/EnumUtil/Sources/SchoolType.swift (1)
  • toSubURL (9-20)
Projects/Shared/TimeTableClient/Sources/SingleTimeTableResponseDTO.swift (1)
  • toDomain (25-33)
Projects/Shared/SchoolClient/Sources/SingleSchoolResponseDTO.swift (1)
  • toDomain (36-44)
Projects/Shared/MealClient/Sources/MealClient.swift (4)
Projects/Shared/TimeTableClient/Sources/TimeTableClient.swift (1)
  • formatDate (16-20)
Projects/Shared/LocalDatabaseClient/Sources/LocalDatabaseClient.swift (4)
  • readRecordByColumn (43-51)
  • save (11-15)
  • save (17-23)
  • readRecordsByColumn (53-61)
Projects/Shared/Entity/Sources/Local/MealLocalEntity.swift (1)
  • toMeal (60-71)
Projects/Shared/NeisClient/Sources/NeisClient.swift (1)
  • fetchDataOnNeis (8-32)
Projects/Shared/Entity/Sources/Local/TimeTableLocalEntity.swift (1)
Projects/Shared/Entity/Sources/Local/MealLocalEntity.swift (1)
  • persistenceKey (37-39)
Projects/Shared/Entity/Sources/Local/MealLocalEntity.swift (1)
Projects/Shared/Entity/Sources/Local/TimeTableLocalEntity.swift (1)
  • persistenceKey (22-24)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: 🧪 Test
🔇 Additional comments (13)
Projects/Shared/Entity/Sources/Local/MealLocalEntity.swift (2)

46-58: 인코딩 실패 시 조용히 빈 배열로 대체됩니다.

try?로 인해 JSON 인코딩 실패가 무시되고 "[]"로 저장됩니다. 이는 의도된 동작일 수 있지만, 디버깅 시 데이터 손실을 알아채기 어려울 수 있습니다.


5-6: Schema already prevents duplicate records for same date via unique constraint with automatic conflict resolution.

The review comment's concerns about duplicate records are unfounded. The database schema in LocalDatabaseClient.swift (line 243) defines the date column as .unique(onConflict: .replace), which automatically replaces any existing record with the same date when inserting. GRDB's insert() method respects this constraint, making explicit upsert logic unnecessary.

The implementation correctly handles the cache-first pattern through schema-level enforcement rather than application logic.

Projects/Shared/MealClient/Project.swift (1)

12-12: LGTM!

캐시 기능 구현을 위해 LocalDatabaseClient 의존성이 올바르게 추가되었습니다.

Projects/Shared/TimeTableClient/Project.swift (1)

15-16: LGTM!

캐시 기능 구현을 위해 UserDefaultsClientLocalDatabaseClient 의존성이 올바르게 추가되었습니다. MealClient와 일관된 패턴입니다.

Projects/Feature/MainFeature/Sources/MainView.swift (1)

53-72: LGTM!

IfLetStore를 사용한 optional state 스코핑이 적절하게 구현되어 있습니다. Weekly view로의 통합이 깔끔하게 이루어졌습니다.

Projects/Shared/LocalDatabaseClient/Sources/LocalDatabaseClient.swift (2)

43-61: LGTM!

GRDB의 Column 필터링을 활용한 새로운 쿼리 헬퍼 메서드들이 잘 구현되어 있습니다. 단일 값 조회와 다중 값 조회 모두 적절한 패턴을 사용하고 있습니다.


240-259: Primary key와 unique 제약조건 충돌 가능성 검토 필요

id가 primary key이고 date가 unique 제약조건을 가지고 있는데, 둘 다 onConflict: .replace를 사용합니다. 동일한 date로 다른 id를 가진 레코드를 삽입하면, unique 제약조건의 replace가 적용되지만 기존 레코드가 다른 primary key를 가지고 있어 예상치 못한 동작이 발생할 수 있습니다.

클라이언트 코드(TimeTableClient, MealClient)에서 저장 전 기존 레코드를 삭제하는 방식으로 이를 처리하고 있으나, insert 대신 GRDB의 save() (insert or update)를 사용하거나, date를 primary key로 사용하는 것이 더 안전할 수 있습니다.

Projects/Shared/TimeTableClient/Sources/TimeTableClient.swift (1)

149-190: 중복 API 호출 패턴에 대한 확인

전공(major) 파라미터를 포함한 첫 번째 API 호출이 실패하면 전공 없이 재시도하는 fallback 패턴으로 보입니다. 의도된 동작이라면 괜찮으나, 향후 유지보수를 위해 이 로직을 별도 헬퍼 함수로 추출하거나 주석을 추가하는 것을 고려해 주세요.

Projects/Shared/MealClient/Sources/MealClient.swift (2)

98-115: 캐시 유효성 검사 로직 검토 필요

hasNonEmptyCache는 모든 캐시된 식단이 비어있지 않아야 true가 됩니다. 특정 날짜에 식단이 제공되지 않는 경우(예: 공휴일, 방학)에도 빈 값으로 캐시될 수 있는데, 이 경우 불필요하게 서버 요청이 발생할 수 있습니다.

의도된 동작인지 확인이 필요합니다. 부분 캐시도 유효하다면 !result.isEmpty만으로 충분할 수 있습니다.


100-124: LGTM!

TimeTableClient와 달리 hasNonEmptyCache 조건으로 백그라운드 동기화를 감싸고 있어, 캐시가 비어있을 때 중복 네트워크 요청이 발생하지 않습니다.

Projects/Feature/MainFeature/Sources/MainCore.swift (3)

153-158: weeklyMealCore / weeklyTimeTableCoreonAppear에서 지연 초기화하는 구조가 적절합니다

nil일 때만 초기화해서 기존 서브 상태(및 캐시)를 재사용하도록 한 덕분에, 의도하신 cache-first 전략과 잘 맞는 구현으로 보입니다.


178-182: 설정 버튼 액션을 하나의 케이스로 합친 부분이 깔끔합니다

상단 탭 액션(.settingButtonDidTap)과 WeeklyMealCore 내부 액션 모두 동일하게 설정 화면을 띄우고 로그를 남기므로, 하나의 분기로 합쳐 처리한 현재 구현은 중복을 줄이면서 의도도 명확하게 드러나 좋습니다.


253-258: 주간 코어만 subFeatures()에 연결한 구조가 의도와 잘 맞습니다

weeklyMealCore / weeklyTimeTableCoreifLet으로 연결해 MainCore를 주간 전용으로 정리한 점이 일관적입니다. 사용하는 TCA 버전에서 action: \.weeklyMealCore / \.weeklyTimeTableCore 형태의 키패스 매핑이 지원되는지만 한 번 확인해 주시면 좋겠습니다.

Copy link
Contributor

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

♻️ Duplicate comments (4)
Projects/Shared/TimeTableClient/Sources/TimeTableClient.swift (3)

16-20: DateFormatter 사용으로 일관성 유지 권장

MealClient에서는 DateFormatter를 사용하는 반면, 여기서는 문자열 조합 방식을 사용하고 있습니다. Locale 처리와 코드베이스 일관성을 위해 DateFormatter 사용을 권장합니다.

 private func formatDate(_ date: Date) -> String {
-    let month = date.month < 10 ? "0\(date.month)" : "\(date.month)"
-    let day = date.day < 10 ? "0\(date.day)" : "\(date.day)"
-    return "\(date.year)\(month)\(day)"
+    let formatter = DateFormatter()
+    formatter.dateFormat = "yyyyMMdd"
+    formatter.locale = Locale(identifier: "ko_kr")
+    return formatter.string(from: date)
 }

149-190: 중첩 do-catch 블록 간소화 가능

첫 번째 요청 실패 시 try?와 nil-coalescing을 사용하면 더 간결하게 표현할 수 있습니다.

     do {
         response = try await neisClient.fetchDataOnNeis(
             type.toSubURL(),
             queryItem: [
                 .init(name: "KEY", value: key),
                 .init(name: "Type", value: "json"),
                 .init(name: "pIndex", value: "1"),
                 .init(name: "pSize", value: "100"),
                 .init(name: "ATPT_OFCDC_SC_CODE", value: orgCode),
                 .init(name: "SD_SCHUL_CODE", value: code),
                 .init(name: "DDDEP_NM", value: major),
                 .init(name: "GRADE", value: "\(grade)"),
                 .init(name: "CLASS_NM", value: "\(`class`)"),
                 .init(name: "TI_FROM_YMD", value: reqDate),
                 .init(name: "TI_TO_YMD", value: reqDate)
             ],
             key: type.toSubURL(),
             type: [SingleTimeTableResponseDTO].self
         )
     } catch {
-        do {
-            response = try await neisClient.fetchDataOnNeis(
+        response = (try? await neisClient.fetchDataOnNeis(
             type.toSubURL(),
             queryItem: [
                 .init(name: "KEY", value: key),
                 ...
             ],
             key: type.toSubURL(),
             type: [SingleTimeTableResponseDTO].self
-            )
-        } catch {
-            response = []
-        }
+        )) ?? []
     }

100-126: 캐시가 비어있을 때 중복 네트워크 요청 발생

Task.detached가 항상 시작되고, 캐시가 비어있으면 fetchTimeTableRangeFromServer도 호출됩니다. 캐시가 없는 경우 동일한 서버 요청이 두 번 발생합니다.

fetchTimeTable과 동일하게 백그라운드 동기화는 캐시된 데이터를 반환하는 경우에만 실행되도록 수정하세요.

+            if !cachedTimeTables.isEmpty {
             Task.detached {
                 await syncTimeTableRangeFromServer(
                     startAt: startAt,
                     endAt: endAt,
                     type: type,
                     orgCode: orgCode,
                     code: code,
                     grade: grade,
                     classNum: `class`,
                     major: major
                 )
             }
+                return cachedTimeTables
+            }

             if cachedTimeTables.isEmpty {
                 return await fetchTimeTableRangeFromServer(
                     startAt: startAt,
                     endAt: endAt,
                     type: type,
                     orgCode: orgCode,
                     code: code,
                     grade: grade,
                     classNum: `class`,
                     major: major
                 )
             }
-
-            return cachedTimeTables
Projects/Shared/MealClient/Sources/MealClient.swift (1)

85-115: hasNonEmptyCache 조건이 지나치게 느슨해 부분 캐시에서도 서버 응답을 즉시 사용하지 않음

현재는 let hasNonEmptyCache = !result.isEmpty로, 요청한 기간 중 하루라도 캐시가 있으면 나머지 날짜는 모두 Meal.empty로 채우고 네트워크 응답은 백그라운드 동기화에만 사용합니다. 이렇게 되면 한 번도 조회한 적 없는 날짜들도 처음에는 빈 값으로 보였다가, 나중에야 실제 급식이 채워지는 패턴이 되어 UX가 다소 예측하기 어려울 수 있습니다.

이전 리뷰 코멘트에서 제안되었던 것처럼, 최소한 result.count == dateStrings.count일 때만 “요청한 모든 날짜가 캐시됨”으로 보고 cache‑first로 동작시키고, 그렇지 않으면 바로 fetchMealsFromServer를 호출해 첫 응답부터 서버 데이터를 반영하는 쪽이 더 직관적으로 보입니다.
물론 현재 구현이 “부분 캐시라도 우선 보여주고, 백그라운드에서 채운다”는 명시적인 의도라면 그대로 유지해도 되므로, 의도와 맞는지 한 번 더 확인해 보시면 좋겠습니다.

🧹 Nitpick comments (3)
Projects/Shared/MealClient/Sources/MealClient.swift (3)

15-20: DateFormatter 재사용 및 날짜 포맷 로직 통합 제안

formatDate(_:)fetchMealsFromServer 내부에서 동일한 포맷(yyyyMMdd, ko_kr)의 DateFormatter를 각각 새로 생성하고 있습니다. 호출 빈도가 높을 수 있으니 정적 프로퍼티나 캐시된 인스턴스로 재사용하고, fetchMealsFromServer에서 formatter.string(from:) 대신 formatDate(_:)를 재사용하면 날짜 포맷 로직이 한 곳에 모여 유지보수성이 좋아집니다.

Also applies to: 187-190


39-51: 단일 날짜 캐시에서 Meal.empty 처리 전략 재점검 권장

단일 조회 경로에서 캐시를 읽은 뒤 !cachedMeal.isEmpty인 경우에만 캐시를 신뢰하고, 그렇지 않으면 매번 fetchMealFromServer를 호출합니다. 현재 fetchMealFromServer는 네트워크 오류나 실제 급식 미제공(공휴일 등) 모두에 대해 Meal.empty를 반환하므로, 공휴일처럼 진짜로 급식이 없는 날에도 매 호출마다 서버를 두드릴 수 있습니다.
Meal.empty가 “정상적으로 급식이 없는 날”을 표현하는 용도로도 쓰인다면, 이 부분을 한 번 더 점검해 보시고 필요하다면 에러/정상 상태를 구분할 수 있는 메타데이터나 플래그를 도입하는 것도 고려해 볼 만합니다.

Also applies to: 129-160


47-49: Task.detached 사용 시 Dependencies 환경 전파 여부 확인 필요

fetchMeal/fetchMeals에서 Task.detachedsyncMealFromServersyncMealsFromServer를 호출하고, 이들 함수 내부에서 @Dependency를 사용하고 있습니다. Point‑Free Dependencies 기준으로는 Task.detached가 의존성 환경을 승계하지 않아, 테스트에서 override한 neisClientlocalDatabaseClient 대신 라이브 의존성이 호출될 수 있는 여지가 있습니다.

가능하다면

  • Task { ... }(structured task)로 변경하거나,
  • Task.detached 안에서 필요한 의존성을 캡처해 파라미터로 넘기는 식으로

백그라운드 작업에서도 동일한 의존성 환경이 보장되도록 조정하는 것을 권장드립니다.

Also applies to: 101-108, 253-264

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 04cb921 and b2b6a9c.

📒 Files selected for processing (3)
  • Projects/App/iOS/Support/Info.plist (2 hunks)
  • Projects/Shared/MealClient/Sources/MealClient.swift (4 hunks)
  • Projects/Shared/TimeTableClient/Sources/TimeTableClient.swift (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
Projects/Shared/MealClient/Sources/MealClient.swift (4)
Projects/Shared/TimeTableClient/Sources/TimeTableClient.swift (1)
  • formatDate (16-20)
Projects/Shared/LocalDatabaseClient/Sources/LocalDatabaseClient.swift (4)
  • readRecordByColumn (43-51)
  • save (11-15)
  • save (17-23)
  • readRecordsByColumn (53-61)
Projects/Shared/Entity/Sources/Local/MealLocalEntity.swift (1)
  • toMeal (60-71)
Projects/Shared/NeisClient/Sources/NeisClient.swift (1)
  • fetchDataOnNeis (8-32)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: ⚙️ Prepare for CI
🔇 Additional comments (5)
Projects/App/iOS/Support/Info.plist (1)

21-22: I'll help you verify the review comment about the Info.plist changes. Let me start by examining the repository structure to understand the multi-target setup and version consistency.
<function_calls>


#!/bin/bash

Find all Info.plist files in the repository

fd 'Info.plist' -t f | sort


</function_calls>

Please wait while I scan for Info.plist files and check version/build consistency across all targets.

Projects/Shared/TimeTableClient/Sources/TimeTableClient.swift (4)

39-59: 캐시 우선 로직이 올바르게 구현됨

캐시된 데이터가 있을 때만 백그라운드 동기화를 트리거하고, 캐시가 비어있으면 서버에서 직접 가져오는 방식이 적절합니다.


197-204: 이전 리뷰 피드백이 반영됨

불필요한 delete 호출이 제거되고 save만 사용하도록 수정되었습니다. date 컬럼의 unique 제약 조건이 중복 데이터를 자동으로 대체합니다.


269-276: 날짜별 그룹화 및 저장 로직 적절함

TimeTable을 날짜별로 그룹화하여 각각 저장하는 방식이 캐시 일관성 유지에 적합합니다. date가 nil인 경우도 적절히 처리됩니다.


281-301: 백그라운드 동기화 래퍼 함수

서버에서 데이터를 가져와 캐시를 업데이트하는 사이드 이펙트만 필요하므로 반환값을 무시하는 것이 적절합니다.

@baekteun baekteun merged commit dfb4a6c into master Dec 7, 2025
@baekteun baekteun deleted the feature/cache-first-meal-timetable branch December 7, 2025 12:24
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