Skip to content

Comments

Style/#39 캘린더 #70

Merged
y-eonee merged 38 commits intodevelopfrom
style/#39-캘린더
Jan 18, 2026

Hidden character warning

The head ref may contain hidden characters: "style/#39-\uce98\ub9b0\ub354"
Merged

Style/#39 캘린더 #70
y-eonee merged 38 commits intodevelopfrom
style/#39-캘린더

Conversation

@y-eonee
Copy link
Contributor

@y-eonee y-eonee commented Jan 15, 2026

🔗 연결된 이슈

📄 작업 내용

  • 캘린더 관련된 UI로직을 구현했습니다
  • 데이터 넣어둔건 전부 mock 이니 참고 정도만 해주세요 ~
구현 내용 IPhone 16 pro IPhone 13 mini
전체 캘린더 플로우
엠티뷰

💻 주요 코드 설명

캘린더 커스텀

https://yyomzzi.tistory.com/20 + 나연언니 서티 코드를 많이 참고했습니다.

연도와 월 가져오기

func getYearAndMonthString() -> String {
// private 함수를 호출해서 다시 프레젠테이션 로직에 맞춰 바꿔주는 역할을 합니다. 
        let targetDate = getCurrentMonth(addingMonth: currentMonth) 
        let calendar = Calendar.current
        let year = calendar.component(.year, from: targetDate)
        let month = calendar.component(.month, from: targetDate)
        return "\(year)\(month)"
    }

한달의 날짜 전부 가져오기

func getDatesArray() -> [DateValue] {
      return extractDate(currentMonth: currentMonth)
 }

private func extractDate(currentMonth: Int) -> [DateValue] {
// 현재 날짜 캘린더 
    let calendar = Calendar.current

// 현재 month 구하기 
    let currentMonth = getCurrentMonth(addingMonth: currentMonth)
// 현재 month가 리턴하는 모든 날짜를 DateValue 객체로 만듬 
    var days = currentMonth.getAllDates().compactMap { date -> DateValue in
        let day = calendar.component(.day, from: date)
 
        return DateValue(day: day, date: date)
    }
    
// days에서 가장 첫날이 시작되는 요일 구하기 
    let firstWeekday = calendar.component(.weekday, from: days.first?.date ?? Date())
    
// month의 가장 첫날이 시작되는 요일 이전을 채웁니다 
// 첫날이 수요일부터 시작되는 경우 일~화 를 -1로 채웁니다 
    for _ in 0 ..< firstWeekday - 1 {
        days.insert(DateValue(day: -1, date: Date()), at: 0)
    }
    
    return days
}

캘린더 뷰모델

@Published private(set) var currentDate: Date = Date() // 현재 날짜
@Published var currentMonth: Int = 0 // 현재 월 
@Published var selectedDate: Date = Calendar.current.startOfDay(for: Date()) // 선택된 날짜 (기본값은 달의 첫날) 
@Published private(set) var procedureCountOfMonth: [Int: Int] = [:] // 한달 내의 시술을 [일:개수]로 저장
@Published private(set) var procedureList: [ProcedureEntity] = [] // 하루의 시술 리스트를 배열로 저장 
@Published private(set) var downtimeByDay: [String : DowntimeDayState] = [:] // 한 시술에 대한 다운타임을 [yyyymmdd:다운타임 Enum] 으로 저장 

스크롤 뷰 내의 그라데이션

struct ScrollTopPreferenceKey: PreferenceKey {
    static var defaultValue: CGFloat = 0
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value = nextValue()
    }
}

struct ScrollBottomPreferenceKey: PreferenceKey {
    static var defaultValue: CGFloat = .zero
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value += nextValue()
    } 
}

Preferencekey를 이용하여 특정 값을 관찰할 수 있습니다. 스크롤뷰 탑과 바텀을 다 보고 있어야하기 때문에 2개를 만들어줬습니다.

 private var scrollViewTopMarkerView: some View {
        GeometryReader { proxy in
            Color.clear
                .onAppear {
                    let v = proxy.frame(in: .global).minY
                    topGlobalY = v
                    if initialTopGlobalY == nil { initialTopGlobalY = v }
                }
                .onChange(of: proxy.frame(in: .global).minY) { v in
                    topGlobalY = v
                }
        }
        .frame(height: 0)
    }
    
    private var scrollViewBottomMarkerView: some View {
        GeometryReader { proxy in
            Color.clear
                .preference(
                    key: ScrollBottomPreferenceKey.self,
                    value: proxy.frame(in: .named("ProcedureScroll")).maxY
                ) 
        }
        .frame(height: 0)
    }

이런 마커 뷰를 보이지 않게 만들어서 기준점 역할을 하게 합니다. Top과 bottom이 좀 다르게 구현 된 이유는
top에서는 화면 기준으로 얼마나 내려왔는지 (global), bottom에서는 스크롤뷰에서 더 내려갈게 있는지 체크하기 때문에 다르게 사용합니다. 마커 뷰를 스크롤뷰 내에서 넣어두고 분기처리 해서 그라데이션 관련 요구사항을 구현했습니다.

캘린더 모드 관리

캘린더 모드를 2가지 상태로 관리합니다.

enum CalendarMode {
    case none // 기본
    case selectedProcedure // 시술 선택 시 
    
    mutating func toggle() {
        switch self {
        case .none:
            self = .selectedProcedure
        case .selectedProcedure:
            self = .none
        }
    }
}

시술을 누를 때마다 기본캘린더로 돌아가거나 시술리스트를 보여주기 때문에 편하게 관리하기 위해 toggle함수를 만들어서 사용했습니다.

그리고 하위 컴포넌트에서 캘린더모드에 따른 렌더링을 관리하기 위해 @binding을 붙여서 사용했습니다.

@y-eonee y-eonee requested review from a team and sum130 January 15, 2026 20:58
@y-eonee y-eonee self-assigned this Jan 15, 2026
@y-eonee y-eonee requested review from soseoyo12 and wotjs020708 and removed request for a team January 15, 2026 20:58
@y-eonee y-eonee linked an issue Jan 15, 2026 that may be closed by this pull request
1 task
@coderabbitai
Copy link

coderabbitai bot commented Jan 15, 2026

📝 Walkthrough

Walkthrough

캘린더 도메인(인터페이스·엔티티·유스케이스), 저장소(기본·모크), DTO, 뷰모델 및 SwiftUI 캘린더 UI(셀·절차뷰·전체뷰)와 관련 DI·ViewFactory 연동을 추가하고 테스트용 UseCase/ViewModel/뷰 및 DefaultTestRepository를 제거했으며 버튼 컴포넌트·자산·유틸 확장을 추가/수정했습니다.

Changes

Cohort / File(s) 변경 요약
Assets - Color
Cherrish-iOS/Cherrish-iOS/Assets.xcassets/Color/gray_0_0.colorset/Contents.json
display‑p3 투명 색상 자산 추가(일반 및 다크 변형)
Assets - Image
Cherrish-iOS/Cherrish-iOS/Assets.xcassets/cherrishIcon/illustration_noschedule.imageset/Contents.json
illustration_noschedule.svg 이미지 항목 추가
Data - DI & Repos
Cherrish-iOS/Cherrish-iOS/Data/DataDependencyAssembler.swift, Cherrish-iOS/Cherrish-iOS/Data/Repository/*, Cherrish-iOS/Cherrish-iOS/Data/Repository/TestRepository.swift
DI 등록을 TestInterfaceCalendarInterface로 전환; MockCalendarRepository·DefaultCalendarRepository 추가, DefaultTestRepository 삭제
Data - DTO
Cherrish-iOS/Cherrish-iOS/Data/Model/CalendarDailyResponseDTO.swift
CalendarDailyResponseDTO, EventResponseDTO 추가(Decodable) 및 빈 toEntity() 확장 추가
Domain - Interface & Model
Cherrish-iOS/Cherrish-iOS/Domain/Interface/CalendarInterface.swift, .../Domain/Interface/TestInterface.swift (삭제), Cherrish-iOS/Cherrish-iOS/Domain/Model/ProcedureEntity.swift, .../Domain/Model/Entity.swift
CalendarInterface 추가, TestInterface 제거, ProcedureEntity 추가, 파일 헤더/Import 정리
Domain - UseCases & DI
Cherrish-iOS/Cherrish-iOS/Domain/UseCase/FetchProcedureCountOfMonth.swift, .../FetchTodayProcedureList.swift, Cherrish-iOS/Cherrish-iOS/Domain/DomainDependencyAssembler.swift
월별 건수·오늘 절차 목록 유스케이스 추가 및 DI 등록(기존 TestUseCase 제거, FetchTodayProcedureList 등록 추가)
Presentation - Calendar UI
Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/*
(예: CalendarCellView.swift, ProcedureView.swift, CalendarViewModel.swift, CalendarView.swift(삭제된 이전 파일))
캘린더 셀·절차 뷰·전체 캘린더 뷰 및 뷰모델 추가(선택 모드, 다운타임 상태, 스크롤 그라디언트, 일정 점표시 등). 기존 placeholder CalendarView 파일 삭제
Presentation - Test
Cherrish-iOS/Cherrish-iOS/Presentation/Feature/TestView.swift, .../TestViewModel.swift
테스트용 뷰·뷰모델 및 관련 UseCase 삭제
Presentation - Components
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Components/CherrishButton.swift, .../GradientBox.swift, .../PreferenceKey.swift
CherrishButton 타입 재정의(large/small/medium), 아이콘(leading/trailing) 추가 및 스타일 변경; GradientBox·스크롤 PreferenceKey 추가
Presentation - Extensions
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/Date+.swift, .../String+.swift
Date.getAllDates(), Date.toDateString(), String.dateFormatter() 추가
Presentation - DI & Factory
Cherrish-iOS/Cherrish-iOS/Presentation/PresentationDependencyAssembler.swift, Cherrish-iOS/Cherrish-iOS/Presentation/ViewFactory.swift
캘린더 유스케이스 해석 및 CalendarViewModel 등록·주입; 테스트 DI 코드 제거 및 ViewFactory에서 CalendarViewModel 주입으로 변경
Call Sites - Button API
Presentation/Feature/** (여러 파일, 예: SelectTreatmentView.swift, NoTreatmentView.swift, TreatmentView.swift, ChallengeProgressView.swift, SelectMissionView.swift, StartChallengeView.swift, HomeView.swift, Onboarding/*.swift)
CherrishButton 호출부 변경: .next/.confirm/.save.large/.small/.medium 전환 및 leadingIcon/trailingIcon 인자 추가(대부분 nil로 전달)

Sequence Diagram(s)

sequenceDiagram
    participant UI as CalendarView
    participant VM as CalendarViewModel
    participant UC_Count as FetchProcedureCountOfMonth
    participant UC_Today as FetchTodayProcedureList
    participant Repo as CalendarRepository

    UI->>VM: onAppear / 월 변경
    VM->>UC_Count: execute(year, month)
    UC_Count->>Repo: fetchProcedureCountOfMonth(year, month)
    Repo-->>UC_Count: [Int: Int]
    UC_Count-->>VM: procedureCountOfMonth 업데이트
    VM-->>UI: 날짜 그리드 렌더

    UI->>VM: 날짜 선택
    VM->>UC_Today: execute(date)
    UC_Today->>Repo: fetchTodayProcedureList(date)
    Repo-->>UC_Today: [ProcedureEntity]
    UC_Today-->>VM: procedureList 업데이트
    VM-->>UI: 일정 리스트 렌더 / downtimeByDay 업데이트
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • wotjs020708
  • soseoyo12
🚥 Pre-merge checks | ✅ 3 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
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.
Linked Issues check ❓ Inconclusive 연결된 이슈 #39는 구체적인 요구사항 없이 'To-Do' 항목만 있고 피그마 캡쳐도 비어있어, 이슈의 구체적인 요구사항을 확인할 수 없습니다. 연결된 이슈 #39의 요구사항이 명확하지 않습니다. PR 설명의 실제 구현 내용이 피그마 디자인 요구사항을 충족하는지 확인이 필요합니다.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목 'Style/#39 캘린더'는 변경 사항의 주요 내용인 캘린더 UI 로직 구현을 명확하게 요약하고 있습니다.
Description check ✅ Passed PR 설명은 캘린더 UI 로직 구현, 데이터는 모두 mock임을 명시하고 있으며, 주요 코드와 구현 내용을 상세히 설명하고 있습니다.
Out of Scope Changes check ✅ Passed PR의 모든 변경사항은 캘린더 기능 구현, 버튼 파라미터 리팩토링, 테스트 코드 제거 등 스타일 이슈 #39와 관련된 범위 내에 있습니다.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch style/#39-캘린더

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.

@sohee6989
Copy link

캘린더 뷰에 틀린 부분 있지롱~~

unnamed

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

Caution

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

⚠️ Outside diff range comments (1)
Cherrish-iOS/Cherrish-iOS/Data/DataDependencyAssembler.swift (1)

11-20: 미사용 networkService 프로퍼티 제거 필요

networkService가 초기화되지만 assemble() 메서드에서 더 이상 사용되지 않습니다. 이전 TestInterface 등록 시 사용되었던 것으로 보이며, 현재는 불필요한 코드입니다.

♻️ 제안된 수정
 final class DataDependencyAssembler: DependencyAssembler {
-    private let networkService: NetworkService
-    
-    init() {
-        self.networkService = DefaultNetworkService()
-    }
     
     func assemble() {
         DIContainer.shared.register(type: CalendarInterface.self) {
             return MockCalendarRepository()
         }
     }
 }

실제 API 연동 시 networkService가 필요하다면, 해당 시점에 다시 추가하는 것이 좋습니다.

🤖 Fix all issues with AI agents
In
`@Cherrish-iOS/Cherrish-iOS/Assets.xcassets/Color/gray_0_0.colorset/Contents.json`:
- Around line 1-38: The color asset named "gray_0_0" is invalid: it breaks
naming conventions (should follow gray_0, gray_100, ..., not gray_0_0) and has
alpha set to "0.000" (fully transparent) while the intended "gray_0" assets use
alpha "1.000"; remove this erroneous asset if it was created accidentally, or if
intended, rename it to "gray_0" and set the "alpha" component to "1.000" in the
Contents.json so it matches the design system and is discoverable by code.

In `@Cherrish-iOS/Cherrish-iOS/Data/Model/CalendarDailyResponseDTO.swift`:
- Around line 27-30: toEntity()가 비어 있어 DTO를 엔티티로 변환하지 못합니다;
CalendarDailyResponseDTO의 toEntity() 구현부를 열어 응답 필드들을 대응되는 엔티티 타입(예:
CalendarDailyEntity 또는 기존 도메인 모델)에 매핑해 반환하도록 구현하거나, 아직 사용되지 않는다면 함수 본문을 제거하거나 호출
시 즉시 발견되도록 fatalError("Not implemented")로 대체하세요; 참조할 식별자는
CalendarDailyResponseDTO::toEntity()이며 변환 시 DTO의 날짜·타입·내용 등 각 프로퍼티를 엔티티의 동일/적절한
프로퍼티로 할당하도록 작성하세요.

In `@Cherrish-iOS/Cherrish-iOS/Domain/DomainDependencyAssembler.swift`:
- Around line 20-22: The guard in DomainDependencyAssembler where you call
DIContainer.shared.resolve(type: CalendarInterface.self) silently returns on
failure; update that branch to log the resolution failure (include the type name
and any context) instead of returning quietly—e.g., use your app logger (or
assertionFailure/print if no logger) inside the else branch before returning so
failures to resolve CalendarInterface are visible; locate the guard around
calendarRepository in DomainDependencyAssembler and add the logging/diagnostic
there.

In `@Cherrish-iOS/Cherrish-iOS/Domain/UseCase/FetchProcedureCountOfMonth.swift`:
- Around line 15-18: Property name has a typo: rename the private property
repopsitory to repository in FetchProcedureCountOfMonth, update the initializer
assignment (init(repository: CalendarInterface) { self.repository = repository
}) and update all internal references in methods of the
FetchProcedureCountOfMonth type to use repository instead of repopsitory to keep
naming consistent and avoid confusion.

In `@Cherrish-iOS/Cherrish-iOS/Domain/UseCase/FetchTodayProcedureList.swift`:
- Around line 10-22: The use-case signature and repository implementation are
inconsistent: DefaultFetchTodayProcedure.execute is declared async throws but
calls a synchronous, non-throwing repository method fetchTodayProcedureList on
CalendarInterface. Fix by choosing one consistent approach: either (A) make
FetchTodayProcedureList.execute synchronous (remove async throws) and return
repository.fetchTodayProcedureList(date:), updating the protocol
FetchTodayProcedureList and DefaultFetchTodayProcedure.execute signatures; or
(B) make CalendarInterface.fetchTodayProcedureList(date:) async throws (and
update all conforming implementations) so DefaultFetchTodayProcedure.execute can
await/throw as declared. Ensure the same decision is applied to both the
FetchTodayProcedureList protocol and the CalendarInterface method declarations
and their implementations.

In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarCellView.swift`:
- Around line 96-107: The title property is using implicit returns in its switch
arms; update the title computed property (var title: String) to use explicit
return statements for each case (.none, .sensitive, .caution, .recovery) so it
matches the style of other properties—add "return" before each string literal
inside the switch and keep the existing case labels and strings unchanged.

In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarView.swift`:
- Line 1: 파일 상단의 불필요한 빈 줄을 제거하세요: CalendarView.swift 파일의 첫 번째 줄에 있는 공백(또는 BOM
포함)을 삭제하여 파일이 바로 import/struct 또는 class 선언(예: CalendarView)으로 시작하도록 하고, 편집기 설정으로
트레일링/리딩 공백이 자동으로 추가되지 않도록 확인하세요.
- Line 133: Rename the misspelled property scheduleListConatinerView to
scheduleListContainerView in CalendarView (update the declaration and every
reference/call site), preserving its type (some View) and access level; ensure
any bindings, previews, or usages (e.g., in body, methods, or subviews) are
updated to the new identifier so the code compiles.
- Around line 257-274: The downTimeRangeIcons view is iterating
DowntimeDayState.allCases and unintentionally rendering the .none case (empty
title/transparent colors), so update the ForEach to exclude .none (e.g., iterate
DowntimeDayState.allCases.filter { $0 != .none } or guard/if to skip when state
== .none) so that the .none state is not created or given a frame; make this
change in the downTimeRangeIcons computed property where ForEach(...) { state in
... } to remove the empty placeholder and avoid extra spacing.
- Around line 28-31: Remove the redundant explicit "= nil" initializers from the
optional `@State` properties (initialTopGlobalY and selectedProcedureID) so they
rely on Swift's default nil initialization; keep non-optional properties
(bottomOffsetY and calendarMode) as-is. Update the declarations for
initialTopGlobalY and selectedProcedureID to simply declare them as optionals
without assigning nil (e.g., `@State` private var initialTopGlobalY: CGFloat? and
`@State` private var selectedProcedureID: Int?). Ensure no other logic depends on
those explicit initializers.
- Around line 170-174: The code uses selectedProcedureID ?? 0 when calling
viewModel.fetchDowntimeByDay, which can pass an invalid ID (0) if
selectedProcedureID is nil; change the call to use the actual
procedure.procedureId from the tapped item (use procedure.procedureId in the
fetchDowntimeByDay call) and ensure selectedProcedureID is still set to
procedure.procedureId before or after calling fetchDowntimeByDay so you never
query with 0 (references: selectedProcedureID, calendarMode.toggle(),
procedure.procedureId, viewModel.fetchDowntimeByDay(procedureId:)).
- Around line 182-185: Remove the temporary debug print in the
onPreferenceChange block and replace it with the app's logging utility: remove
the line print("topOffsetY(named):", v) in the closure that observes
ScrollTopPreferenceKey and either drop logging entirely or call CherrishLogger
(e.g., CherrishLogger.debug) with a concise message including the value; ensure
you keep the existing logic that sets topGlobalY and do not alter
onPreferenceChange or ScrollTopPreferenceKey usage.

In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/ProcedureView.swift`:
- Line 78: The code uses an extra space between the initializer name and its
parentheses in RoundedRectangle (cornerRadius: 6); update the usage of
RoundedRectangle in ProcedureView (e.g., the RoundedRectangle initializer) to
remove the space so it reads RoundedRectangle(cornerRadius: 6) to match Swift
style conventions.

In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarViewModel.swift`:
- Around line 126-142: In extractDate(currentMonth: Int) the local constant
currentMonth shadows the function parameter; rename the local result of
getCurrentMonth(addingMonth: currentMonth) (for example to monthDates or
targetMonth) and update all references in this function (the .getAllDates() call
and any subsequent usage like firstWeekday calculation) so the parameter name is
no longer shadowed and the code is clearer (keep the function signature as
extractDate(currentMonth: Int) and only change the local identifier).
- Around line 60-66: The function firstDateOfCurrentMonth() force-unwraps
calendar.date(from: components), which can crash if components are invalid;
change it to safely unwrap (e.g., use guard let firstDate = calendar.date(from:
components) else { return /* sensible fallback */ } ) and return that unwrapped
Date or propagate an optional/throw, ensuring you reference
getCurrentMonth(addingMonth: currentMonth) and the calendar.date(from:
components) call when making the fix.
- Around line 117-124: The private method getYearAndMonth(currenDate:) is unused
and has a parameter-name typo; either delete the method entirely if not needed,
or rename the parameter to currentDate (change signature to
getYearAndMonth(currentDate: Date) -> [String]) and update any future call sites
to use the corrected name; keep the body (DateFormatter/date.components) as-is
if you choose to keep it.

In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Global/Components/CherrishButton.swift`:
- Around line 26-28: The optional properties leadingIcon and trailingIcon lack
defaults causing callers to break; update CherrishButton by either giving the
stored properties default values (e.g., var leadingIcon: Image? = nil; var
trailingIcon: Image? = nil) or add/modify the custom initializer
(init(title:type:state:leadingIcon:trailingIcon:action:)) to provide default nil
for leadingIcon and trailingIcon so existing call sites compile without
supplying icons; ensure you update the initializer signature used by
ButtonState/CherrishButtonType consumers (and any references to
leadingIcon/trailingIcon) accordingly.
- Around line 36-48: The icons (leadingIcon/trailingIcon) are missing the color
modifier so they render in their original colors instead of matching the button
state; update the leadingIcon and trailingIcon views in CherrishButton to apply
the same color as the text by chaining .foregroundStyle(type.textColor(for:
state)) to each icon and, if those icons are SwiftUI Image instances, ensure
they use .renderingMode(.template) before applying the foregroundStyle so
tinting works correctly.

In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Global/Components/PreferenceKey.swift`:
- Around line 17-20: ScrollBottomPreferenceKey.reduce currently accumulates
values with "+=", which can distort position when multiple views provide values;
change the reduction to use a non-accumulating strategy (e.g., take the maximum
or the latest value) by replacing the "+=" behavior in
ScrollBottomPreferenceKey.reduce so it sets value to max(value, nextValue()) or
simply assigns nextValue() depending on intent, ensuring you update the reduce
implementation on the ScrollBottomPreferenceKey type.

In `@Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/Date`+.swift:
- Around line 11-17: The getAllDates() implementation force-unwraps
calendar.date(from:) and calendar.range(of:) and uses Date() as a fallback,
which can crash or corrupt calendar results; change getAllDates() to safely
unwrap these calls (use guard let startDate = calendar.date(from:
Calendar.current.dateComponents([.year, .month], from: self)) and guard let
range = calendar.range(of: .day, in: .month, for: startDate)) and return an
empty array (or propagate an error) if they fail, and remove the Date() fallback
inside the compactMap so you only include non-nil results from
calendar.date(byAdding: .day, value: day - 1, to: startDate).

In `@Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/String`+.swift:
- Around line 11-24: The dateFormatter() implementation creates new
DateFormatter instances on each call and omits explicit timeZone handling;
change it to use cached/static DateFormatter instances (e.g., static let
inputFormatter and static let outputFormatter) to avoid per-call allocation and
set explicit timeZone values on both formatters (for example
inputFormatter.timeZone = TimeZone(secondsFromGMT: 0) or server's TZ and
outputFormatter.timeZone = TimeZone(identifier: "Asia/Seoul")) while keeping the
existing locale settings; update the function to parse with the cached
inputFormatter and format with the cached outputFormatter (referencing the
symbols dateFormatter(), inputFormatter, outputFormatter, DateFormatter, locale,
timeZone).

In
`@Cherrish-iOS/Cherrish-iOS/Presentation/PresentationDependencyAssembler.swift`:
- Around line 24-35: In assemble(), avoid capturing
fetchProcedureCountOfMonthUseCase and fetchTodayProcedureListUseCase at
registration time; instead inside DIContainer.shared.register(type:
CalendarViewModel.self) closure call DIContainer.shared.resolve(...) for
FetchProcedureCountOfMonth and FetchTodayProcedureList and pass those resolved
instances into CalendarViewModel(initializer) so the registration uses lazy
resolution and won't hold stale dependencies across reassembly or scope changes.

In `@Cherrish-iOS/Cherrish-iOS/Presentation/ViewFactory.swift`:
- Around line 45-48: The guard that resolves CalendarViewModel currently calls
bare fatalError(), making crashes hard to diagnose; update the failure path in
ViewFactory where DIContainer.shared.resolve(type: CalendarViewModel.self) is
unwrapped so it logs or calls fatalError with a descriptive message (e.g.
"Failed to resolve CalendarViewModel via DIContainer.shared.resolve") and
include contextual info like the view name (CalendarView) or container state;
target the guard around CalendarViewModel resolution and the return
CalendarView(viewModel:) to add the informative message.
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b13a523 and 3e18904.

⛔ Files ignored due to path filters (1)
  • Cherrish-iOS/Cherrish-iOS/Assets.xcassets/cherrishIcon/illustration_noschedule.imageset/illustration_noschedule.svg is excluded by !**/*.svg
📒 Files selected for processing (32)
  • Cherrish-iOS/Cherrish-iOS/Assets.xcassets/Color/gray_0_0.colorset/Contents.json
  • Cherrish-iOS/Cherrish-iOS/Assets.xcassets/cherrishIcon/illustration_noschedule.imageset/Contents.json
  • Cherrish-iOS/Cherrish-iOS/Data/DataDependencyAssembler.swift
  • Cherrish-iOS/Cherrish-iOS/Data/Model/CalendarDailyResponseDTO.swift
  • Cherrish-iOS/Cherrish-iOS/Data/Repository/CalendarRepository.swift
  • Cherrish-iOS/Cherrish-iOS/Data/Repository/TestRepository.swift
  • Cherrish-iOS/Cherrish-iOS/Domain/DomainDependencyAssembler.swift
  • Cherrish-iOS/Cherrish-iOS/Domain/Interface/CalendarInterface.swift
  • Cherrish-iOS/Cherrish-iOS/Domain/Interface/TestInterface.swift
  • Cherrish-iOS/Cherrish-iOS/Domain/Model/Entity.swift
  • Cherrish-iOS/Cherrish-iOS/Domain/Model/ProcedureEntity.swift
  • Cherrish-iOS/Cherrish-iOS/Domain/UseCase/FetchProcedureCountOfMonth.swift
  • Cherrish-iOS/Cherrish-iOS/Domain/UseCase/FetchTodayProcedureList.swift
  • Cherrish-iOS/Cherrish-iOS/Domain/UseCase/TestUseCase.swift
  • Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarCellView.swift
  • Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarView.swift
  • Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/ProcedureView.swift
  • Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarView.swift
  • Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarViewModel.swift
  • Cherrish-iOS/Cherrish-iOS/Presentation/Feature/TestView.swift
  • Cherrish-iOS/Cherrish-iOS/Presentation/Feature/TestViewModel.swift
  • Cherrish-iOS/Cherrish-iOS/Presentation/Global/Components/CherrishButton.swift
  • Cherrish-iOS/Cherrish-iOS/Presentation/Global/Components/CherrishTabbar/CherrishTabBar.swift
  • Cherrish-iOS/Cherrish-iOS/Presentation/Global/Components/CherrishTabbar/TabBarCoordinator.swift
  • Cherrish-iOS/Cherrish-iOS/Presentation/Global/Components/CherrishTabbar/TabBarCoordinatorView.swift
  • Cherrish-iOS/Cherrish-iOS/Presentation/Global/Components/GradientBox.swift
  • Cherrish-iOS/Cherrish-iOS/Presentation/Global/Components/PreferenceKey.swift
  • Cherrish-iOS/Cherrish-iOS/Presentation/Global/Components/ProgressBar.swift
  • Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/Date+.swift
  • Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/String+.swift
  • Cherrish-iOS/Cherrish-iOS/Presentation/PresentationDependencyAssembler.swift
  • Cherrish-iOS/Cherrish-iOS/Presentation/ViewFactory.swift
💤 Files with no reviewable changes (7)
  • Cherrish-iOS/Cherrish-iOS/Domain/Interface/TestInterface.swift
  • Cherrish-iOS/Cherrish-iOS/Domain/Model/Entity.swift
  • Cherrish-iOS/Cherrish-iOS/Data/Repository/TestRepository.swift
  • Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarView.swift
  • Cherrish-iOS/Cherrish-iOS/Domain/UseCase/TestUseCase.swift
  • Cherrish-iOS/Cherrish-iOS/Presentation/Feature/TestView.swift
  • Cherrish-iOS/Cherrish-iOS/Presentation/Feature/TestViewModel.swift
🧰 Additional context used
🧬 Code graph analysis (9)
Cherrish-iOS/Cherrish-iOS/Domain/Interface/CalendarInterface.swift (1)
Cherrish-iOS/Cherrish-iOS/Data/Repository/CalendarRepository.swift (4)
  • fetchProcedureCountOfMonth (11-13)
  • fetchProcedureCountOfMonth (21-29)
  • fetchTodayProcedureList (15-17)
  • fetchTodayProcedureList (31-119)
Cherrish-iOS/Cherrish-iOS/Domain/UseCase/FetchProcedureCountOfMonth.swift (1)
Cherrish-iOS/Cherrish-iOS/Data/Repository/CalendarRepository.swift (2)
  • fetchProcedureCountOfMonth (11-13)
  • fetchProcedureCountOfMonth (21-29)
Cherrish-iOS/Cherrish-iOS/Data/DataDependencyAssembler.swift (1)
Cherrish-iOS/Cherrish-iOS/Core/DIContainer.swift (1)
  • register (22-25)
Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarCellView.swift (2)
Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarViewModel.swift (1)
  • isSelected (39-41)
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/View+Color.swift (7)
  • gray1000 (49-51)
  • gray500 (29-31)
  • red700 (89-91)
  • red500 (81-83)
  • red300 (73-75)
  • red200 (69-71)
  • red400 (77-79)
Cherrish-iOS/Cherrish-iOS/Domain/DomainDependencyAssembler.swift (1)
Cherrish-iOS/Cherrish-iOS/Core/DIContainer.swift (2)
  • resolve (27-34)
  • register (22-25)
Cherrish-iOS/Cherrish-iOS/Domain/UseCase/FetchTodayProcedureList.swift (1)
Cherrish-iOS/Cherrish-iOS/Data/Repository/CalendarRepository.swift (2)
  • fetchTodayProcedureList (15-17)
  • fetchTodayProcedureList (31-119)
Cherrish-iOS/Cherrish-iOS/Presentation/PresentationDependencyAssembler.swift (2)
Cherrish-iOS/Cherrish-iOS/Core/DIContainer.swift (2)
  • resolve (27-34)
  • register (22-25)
Cherrish-iOS/Cherrish-iOS/Core/CherrishLogger.swift (1)
  • error (147-153)
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Components/CherrishButton.swift (2)
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/Font+.swift (1)
  • typography (60-65)
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/View+Color.swift (4)
  • red700 (89-91)
  • gray200 (17-19)
  • gray0 (9-11)
  • gray600 (33-35)
Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarViewModel.swift (2)
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/Date+.swift (2)
  • toDateString (20-27)
  • getAllDates (11-18)
Cherrish-iOS/Cherrish-iOS/Core/CherrishLogger.swift (1)
  • debug (131-137)
🪛 SwiftLint (0.57.0)
Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarView.swift

[Warning] 28-28: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)


[Warning] 31-31: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)

🔇 Additional comments (16)
Cherrish-iOS/Cherrish-iOS/Assets.xcassets/cherrishIcon/illustration_noschedule.imageset/Contents.json (1)

1-11: 문제 없습니다.

표준 Xcode 에셋 카탈로그 구조를 따르고 있어 이대로 진행해도 괜찮습니다.

Cherrish-iOS/Cherrish-iOS/Presentation/Global/Components/GradientBox.swift (1)

10-18: 구현이 단순하고 명확합니다.

의도(상/하단 그라데이션)와 코드가 잘 맞습니다.

Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/Date+.swift (1)

20-26: 포맷/타임존/로케일 설정이 명확합니다.

의도한 출력 포맷이 잘 드러납니다.

Cherrish-iOS/Cherrish-iOS/Domain/Model/ProcedureEntity.swift (1)

10-16: 모델 정의가 명확합니다.

필드 구성과 Hashable 채택이 적절합니다.

Cherrish-iOS/Cherrish-iOS/Domain/Interface/CalendarInterface.swift (1)

10-12: 프로토콜 계약이 간결합니다.

필요한 의도가 분명합니다.

Cherrish-iOS/Cherrish-iOS/Data/Repository/CalendarRepository.swift (2)

10-18: DefaultCalendarRepository 스텁 구현 확인

기본 Repository가 빈 데이터를 반환하는 스텁 구현으로 적절합니다. 향후 실제 API 연동 시 이 구현체를 업데이트하면 됩니다.


20-120: MockCalendarRepository는 UI 테스트용으로 적절합니다.

Mock 데이터가 다양한 시술 유형과 회복 기간을 포함하여 UI 테스트에 유용합니다. 다만, 하드코딩된 날짜("2026-01-15" 등)는 시간이 지나면 테스트 시나리오가 현재 날짜와 맞지 않을 수 있습니다.

향후 개선 사항으로, 현재 날짜 기준으로 동적 날짜를 생성하는 방식을 고려해 보세요:

// 예시: 현재 날짜 기준 동적 날짜 생성
let today = Date()
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
let sensitiveDays = (0...2).map { 
    formatter.string(from: Calendar.current.date(byAdding: .day, value: $0, to: today)!) 
}
Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarViewModel.swift (2)

103-115: getCurrentMonthcurrentDate 대신 Date()를 사용

getCurrentMonth(addingMonth:) 메서드가 저장된 currentDate 프로퍼티 대신 Date()를 직접 사용합니다. 이로 인해 currentDate와 계산된 월 간에 불일치가 발생할 수 있습니다.

의도적인 설계라면 무시해도 됩니다. 그렇지 않다면 아래와 같이 수정을 고려해 주세요:

     private func getCurrentMonth(addingMonth: Int) -> Date {
         let calendar = Calendar.current
         
         guard let currentMonth = calendar.date(
             byAdding: .month,
             value: addingMonth,
-            to: Date()
+            to: currentDate
         ) else {
-            return Date()
+            return currentDate
         }
         
         return currentMonth
     }

144-151: 다운타임 상태 덮어쓰기 순서 확인 필요

mapToDowntimeDays에서 같은 날짜에 여러 상태가 있을 경우, 나중에 처리되는 상태가 이전 상태를 덮어씁니다. 현재 순서: sensitivecautionrecovery.

이 순서가 의도된 우선순위라면 괜찮지만, 그렇지 않다면 우선순위 로직을 명시적으로 처리해야 합니다.

우선순위가 recovery > caution > sensitive가 맞는지 확인해 주세요.

Cherrish-iOS/Cherrish-iOS/Domain/DomainDependencyAssembler.swift (1)

24-30: Use case 등록 구조가 적절합니다.

DI 컨테이너 패턴을 올바르게 사용하고 있으며, use case들이 repository 의존성과 함께 적절히 등록되어 있습니다.

Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarCellView.swift (2)

37-37: padding 셀에서 day가 -1일 때 표시 문제

value.day가 -1인 padding 셀의 경우, 이 뷰가 호출되면 "-1"이 화면에 표시될 수 있습니다. CalendarView에서 value.day != -1 조건으로 필터링하고 있지만, 방어적 코딩을 위해 뷰 내부에서도 유효성 검사를 고려해주세요.


10-15: DowntimeDayState enum 정의가 적절합니다.

CaseIterableHashable 프로토콜을 채택하여 ForEach 등에서 활용할 수 있도록 잘 설계되었습니다.

Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarView.swift (1)

11-23: CalendarMode enum 설계가 적절합니다.

toggle() 메서드를 통한 상태 전환 로직이 깔끔하게 구현되어 있습니다.

Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/ProcedureView.swift (3)

10-13: ProcedureStatus enum 설계가 적절합니다.

상태 기반 스타일링을 위한 enum 설계가 깔끔하고, extension을 통한 색상 매핑이 잘 구성되어 있습니다.


22-33: status 계산 프로퍼티 로직이 명확합니다.

선택 상태와 calendarMode에 따른 상태 결정 로직이 잘 구현되어 있습니다.


64-71: downtime 조건부 렌더링 로직이 적절합니다.

status == .active일 때만 downtime 정보를 표시하고, downTimeDays == 0인 경우 "-"를 표시하는 로직이 명확합니다.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +1 to +38
{
"colors" : [
{
"color" : {
"color-space" : "display-p3",
"components" : {
"alpha" : "0.000",
"blue" : "0xFF",
"green" : "0xFF",
"red" : "0xFF"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "display-p3",
"components" : {
"alpha" : "0.000",
"blue" : "0xFF",
"green" : "0xFF",
"red" : "0xFF"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# 색상 자산 파일들을 찾아서 명명 규칙 파악
find . -path "*/Assets.xcassets/Color/*" -name "Contents.json" | head -20

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 1682


🏁 Script executed:

# gray_0_0 색상이 코드베이스에서 어디에 사용되는지 확인
rg "gray_0_0|gray-0-0" --type swift --type objc -i

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 52


🏁 Script executed:

# 설계 시스템 문서나 색상 명세 파일 찾기
fd -e md -e txt -e json -e yaml -e yml | xargs rg -l "design|color|palette|gray" -i | grep -iE "design|spec|guide|readme" | head -10

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 52


🏁 Script executed:

# gray_0_0 파일 확인
cat ./Cherrish-iOS/Cherrish-iOS/Assets.xcassets/Color/gray_0_0.colorset/Contents.json

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 770


🏁 Script executed:

# gray_0과 다른 회색 자산들의 색상 값 비교
echo "=== gray_0 ===" && cat ./Cherrish-iOS/Cherrish-iOS/Assets.xcassets/Color/gray_0.colorset/Contents.json | jq '.colors[0].color.components'
echo "=== gray_100 ===" && cat ./Cherrish-iOS/Cherrish-iOS/Assets.xcassets/Color/gray_100.colorset/Contents.json | jq '.colors[0].color.components'
echo "=== gray_200 ===" && cat ./Cherrish-iOS/Cherrish-iOS/Assets.xcassets/Color/gray_200.colorset/Contents.json | jq '.colors[0].color.components'
echo "=== gray_900 ===" && cat ./Cherrish-iOS/Cherrish-iOS/Assets.xcassets/Color/gray_900.colorset/Contents.json | jq '.colors[0].color.components'
echo "=== gray_1000 ===" && cat ./Cherrish-iOS/Cherrish-iOS/Assets.xcassets/Color/gray_1000.colorset/Contents.json | jq '.colors[0].color.components'

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 538


🏁 Script executed:

# gray_0_0에 대한 모든 참조 검색 (파일명 포함)
rg "gray.0.0|gray_0_0" -i --type json

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 52


gray_0_0 파일은 설계 시스템 규칙을 위반하며 중대한 오류를 포함합니다.

이 색상 자산은 여러 문제를 가지고 있습니다:

  1. 명명 규칙 위반: 프로젝트의 색상 자산은 gray_0, gray_100, gray_200 ... gray_1000과 같은 gray_X 패턴을 따릅니다. gray_0_0의 이중 언더스코어와 추가 0은 이 규칙에 맞지 않으며, gray_0의 오류로 인한 중복 파일처럼 보입니다.

  2. 투명도 값 오류 (중대): alpha : "0.000"으로 설정되어 완전히 투명하고 보이지 않습니다. 비교 대상인 gray_0 (및 모든 다른 회색 자산)은 alpha : "1.000"(불투명)입니다. 이는 설계 시스템 전체와 불일치합니다.

  3. 사용되지 않는 자산: 코드베이스 전체에서 이 색상에 대한 참조가 없습니다.

권장 사항: 이 파일이 오류로 생성된 것이라면 삭제하고, 의도된 자산이라면 명명을 gray_0으로 변경하고 alpha"1.000"으로 수정해야 합니다.

🤖 Prompt for AI Agents
In
`@Cherrish-iOS/Cherrish-iOS/Assets.xcassets/Color/gray_0_0.colorset/Contents.json`
around lines 1 - 38, The color asset named "gray_0_0" is invalid: it breaks
naming conventions (should follow gray_0, gray_100, ..., not gray_0_0) and has
alpha set to "0.000" (fully transparent) while the intended "gray_0" assets use
alpha "1.000"; remove this erroneous asset if it was created accidentally, or if
intended, rename it to "gray_0" and set the "alpha" component to "1.000" in the
Contents.json so it matches the design system and is discoverable by code.

Comment on lines +27 to +30
extension CalendarDailyResponseDTO {
func toEntity() {

}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

toEntity()가 비어 있어 변환이 누락됩니다.

현재는 호출해도 아무 동작을 하지 않습니다. 실제 변환 로직을 구현하거나, 아직 미사용이라면 제거/fatalError("Not implemented") 등으로 조기 탐지를 권장합니다.

🤖 Prompt for AI Agents
In `@Cherrish-iOS/Cherrish-iOS/Data/Model/CalendarDailyResponseDTO.swift` around
lines 27 - 30, toEntity()가 비어 있어 DTO를 엔티티로 변환하지 못합니다; CalendarDailyResponseDTO의
toEntity() 구현부를 열어 응답 필드들을 대응되는 엔티티 타입(예: CalendarDailyEntity 또는 기존 도메인 모델)에 매핑해
반환하도록 구현하거나, 아직 사용되지 않는다면 함수 본문을 제거하거나 호출 시 즉시 발견되도록 fatalError("Not
implemented")로 대체하세요; 참조할 식별자는 CalendarDailyResponseDTO::toEntity()이며 변환 시 DTO의
날짜·타입·내용 등 각 프로퍼티를 엔티티의 동일/적절한 프로퍼티로 할당하도록 작성하세요.

Comment on lines +20 to 22
guard let calendarRepository = DIContainer.shared.resolve(type: CalendarInterface.self) else {
return
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

CalendarInterface 해결 실패 시 조용히 반환됨

CalendarInterface 해결이 실패하면 use case들이 등록되지 않고 조용히 반환됩니다. 이로 인해 런타임에 디버깅하기 어려운 문제가 발생할 수 있습니다. 실패 시 로깅을 추가하는 것을 고려해주세요.

♻️ 제안하는 수정
-        guard let calendarRepository = DIContainer.shared.resolve(type: CalendarInterface.self) else {
+        guard let calendarRepository = DIContainer.shared.resolve(type: CalendarInterface.self) else {
+            CherrishLogger.error("Failed to resolve CalendarInterface")
             return
         }
🤖 Prompt for AI Agents
In `@Cherrish-iOS/Cherrish-iOS/Domain/DomainDependencyAssembler.swift` around
lines 20 - 22, The guard in DomainDependencyAssembler where you call
DIContainer.shared.resolve(type: CalendarInterface.self) silently returns on
failure; update that branch to log the resolution failure (include the type name
and any context) instead of returning quietly—e.g., use your app logger (or
assertionFailure/print if no logger) inside the else branch before returning so
failures to resolve CalendarInterface are visible; locate the guard around
calendarRepository in DomainDependencyAssembler and add the logging/diagnostic
there.

Comment on lines +10 to +22
protocol FetchTodayProcedureList {
func execute(date: String) async throws -> [ProcedureEntity]
}

struct DefaultFetchTodayProcedure: FetchTodayProcedureList {
private let repository: CalendarInterface

init(repository: CalendarInterface) {
self.repository = repository
}

func execute(date: String) async throws -> [ProcedureEntity] {
repository.fetchTodayProcedureList(date: date)
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

비동기/throws 시그니처 정합성을 확인해주세요.

현재 레포지토리 메서드가 동기/비에러인데 async throws를 노출하면 호출부가 불필요하게 await/try를 강제하게 됩니다. 레포지토리를 async/throws로 올릴지, 사용 사례 시그니처를 동기로 내릴지 맞춰주세요.

♻️ (동기화 선택 시) 제안 수정안
-protocol FetchTodayProcedureList {
-    func execute(date: String) async throws -> [ProcedureEntity]
-}
+protocol FetchTodayProcedureList {
+    func execute(date: String) -> [ProcedureEntity]
+}

 struct DefaultFetchTodayProcedure: FetchTodayProcedureList {
@@
-    func execute(date: String) async throws -> [ProcedureEntity] {
+    func execute(date: String) -> [ProcedureEntity] {
         repository.fetchTodayProcedureList(date: date)
     }
 }
🤖 Prompt for AI Agents
In `@Cherrish-iOS/Cherrish-iOS/Domain/UseCase/FetchTodayProcedureList.swift`
around lines 10 - 22, The use-case signature and repository implementation are
inconsistent: DefaultFetchTodayProcedure.execute is declared async throws but
calls a synchronous, non-throwing repository method fetchTodayProcedureList on
CalendarInterface. Fix by choosing one consistent approach: either (A) make
FetchTodayProcedureList.execute synchronous (remove async throws) and return
repository.fetchTodayProcedureList(date:), updating the protocol
FetchTodayProcedureList and DefaultFetchTodayProcedure.execute signatures; or
(B) make CalendarInterface.fetchTodayProcedureList(date:) async throws (and
update all conforming implementations) so DefaultFetchTodayProcedure.execute can
await/throw as declared. Ensure the same decision is applied to both the
FetchTodayProcedureList protocol and the CalendarInterface method declarations
and their implementations.

Comment on lines +11 to +17
func getAllDates() -> [Date] {
let calendar = Calendar.current
let startDate = calendar.date(from: Calendar.current.dateComponents([.year, .month], from: self))!
let range = calendar.range(of: .day, in: .month, for: startDate)!
return range.compactMap { day -> Date in
calendar.date(byAdding: .day, value: day - 1, to: startDate) ?? Date()
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

git ls-files "*Date+.swift" | head -20

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 137


🏁 Script executed:

find . -name "Date+.swift" -type f 2>/dev/null

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 139


🏁 Script executed:

find . -path "*/Presentation/Global/Extension/*" -name "*.swift" 2>/dev/null

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 579


🏁 Script executed:

cat -n "Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/Date+.swift"

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 1069


🏁 Script executed:

rg "getAllDates" "Cherrish-iOS" --type swift -B 2 -A 2

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 1281


🏁 Script executed:

rg "getAllDates\(\)" "Cherrish-iOS" --type swift

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 332


🏁 Script executed:

cat -n "Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarViewModel.swift" | grep -A 5 -B 5 "getAllDates"

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 634


🏁 Script executed:

python3 << 'EOF'
# Check if the Calendar operations can actually return nil
# in normal circumstances with valid date inputs

import json

# Swift Calendar API behavior:
# calendar.date(from:) - Returns nil if components are invalid
# calendar.range(of: .day, in: .month, for:) - Returns nil in edge cases

# Potential failure scenarios:
scenarios = {
    "dateComponents with valid year/month": {
        "description": "Creating date from valid year/month components",
        "can_fail": "Unlikely - these are basic components",
        "actual_risk": "Low"
    },
    "calendar.range for month": {
        "description": "Getting day range for a month",
        "can_fail": "Extremely rare - all months have defined day ranges",
        "actual_risk": "Very Low"
    },
    "byAdding with day value": {
        "description": "Adding days to a valid date",
        "can_fail": "Theoretically possible with extreme calendar systems",
        "actual_risk": "Low to Medium"
    },
    "Date() fallback": {
        "description": "Using current time as default",
        "can_fail": "No - but semantically wrong",
        "actual_risk": "Data Integrity Issue"
    }
}

for scenario, details in scenarios.items():
    print(f"\n{scenario}:")
    print(f"  {details['description']}")
    print(f"  Can fail: {details['can_fail']}")
    print(f"  Risk: {details['actual_risk']}")

print("\n\nKey Issue: The Date() fallback is incorrect even if nil is rare")
print("Using current time as fallback instead of proper error handling")
print("This can silently corrupt calendar data")
EOF

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 156


🏁 Script executed:

# Check Swift documentation and common calendar issues
web_search "Swift Calendar.date nil cases dateComponents force unwrap"

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 120


🌐 Web query:

Swift Calendar.date dateComponents nil when can fail

💡 Result:

Short answer — Calendar.date(from:) (and DateComponents.date) can return nil when the components can't be unambiguously converted to a valid Date. Common causes:

  • Required fields missing for that calendar (e.g. no year/month/day for a Gregorian date). See Calendar API. [1]
  • Components are inconsistent/conflicting (weekday/weekOfYear vs year/month/day etc.); the calendrical algorithm can't reconcile them. Validate/trim to only the fields you actually mean to set. [2]
  • Out-of-range values (month = 13, day = 0, nanosecond ≥ 1_000_000_000, etc.). Bad component ranges can fail or behave unexpectedly. [3]
  • Local-time DST gaps / ambiguous times (a local hour that doesn't exist during a DST forward shift may be invalid). See Calendar/timeZone behavior in docs. [1]
  • Platform/implementation bugs (behavior differences or crashes reported on non‑Apple platforms / corelibs-foundation). [3][4]

How to avoid/fix:

  • Use DateComponents.isValidDate(in:) or Calendar.date(from:) and check for nil. [1][2]
  • Only set the exact components you need (avoid carrying extra fields like weekday/weekOfYear). [2]
  • Normalize/validate numeric ranges (nanoseconds < 1_000_000_000, 1…12 months, valid day for month).
  • For platform portability, prefer constructing dates via Calendar methods (date(byAdding:to:), date(from:)) with an explicit Calendar(identifier:) and timeZone, and test on targets you support. [3]

Sources: Apple Calendar/DateComponents docs and Swift community discussion about inconsistent components and platform issues. [1][2][3][4]

References
[1] Apple Developer — Calendar / DateComponents docs.
[2] Swift Forums — “Date from date components is incorrect …” (explanation about inconsistent fields).
[3] Swift Forums — reports about crashes / nil on Linux when creating Date from DateComponents (nanosecond/out‑of‑range and corelibs issues).
[4] StackOverflow / Swift forum discussion about DateComponents value() / bridging quirks.


🌐 Web query:

Swift Calendar.range day month force unwrap safety

💡 Result:

Short answer: don’t force‑unwrap Calendar.range(...). It returns Range? and can be nil (for example when the “larger” component isn’t logically bigger than the “smaller” one, or if the date/components are invalid), so force‑unwrapping risks a runtime crash. Use optional binding/guard or use dateInterval(of:for:) and safer Date creation. [1][2]

Safe patterns (examples):

  • Optional binding
    guard let range = calendar.range(of: .day, in: .month, for: date) else { /* handle error */ }
    let days = range.count

  • Nil coalescing (fallback)
    let days = calendar.range(of: .day, in: .month, for: date)?.count ?? 0

  • Use dateInterval when you need start/end
    if let interval = calendar.dateInterval(of: .month, for: date) { /* use interval.start/interval.end */ }

Sources: corelibs Foundation Calendar implementation and community examples. [1][2]


달력 환경에 따라 force unwrap과 Date() fallback이 실제 위험입니다.

calendar.date(from:)calendar.range(of:)는 구성 요소 누락, 모순된 값, 범위 초과, DST 간격 등으로 nil을 반환할 수 있습니다. 강제 언래핑은 크래시 위험이 있고, Date() 폴백은 현재 시간을 기본값으로 사용하여 달력 데이터를 손상시킵니다.

✅ 제안 수정
 func getAllDates() -> [Date] {
     let calendar = Calendar.current
-    let startDate = calendar.date(from: Calendar.current.dateComponents([.year, .month], from: self))!
-    let range = calendar.range(of: .day, in: .month, for: startDate)!
-    return range.compactMap { day -> Date in
-        calendar.date(byAdding: .day, value: day - 1, to: startDate) ?? Date()
-    }
+    guard let startDate = calendar.date(from: calendar.dateComponents([.year, .month], from: self)),
+          let range = calendar.range(of: .day, in: .month, for: startDate)
+    else { return [] }
+    return range.compactMap { day in
+        calendar.date(byAdding: .day, value: day - 1, to: startDate)
+    }
 }
📝 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
func getAllDates() -> [Date] {
let calendar = Calendar.current
let startDate = calendar.date(from: Calendar.current.dateComponents([.year, .month], from: self))!
let range = calendar.range(of: .day, in: .month, for: startDate)!
return range.compactMap { day -> Date in
calendar.date(byAdding: .day, value: day - 1, to: startDate) ?? Date()
}
func getAllDates() -> [Date] {
let calendar = Calendar.current
guard let startDate = calendar.date(from: calendar.dateComponents([.year, .month], from: self)),
let range = calendar.range(of: .day, in: .month, for: startDate)
else { return [] }
return range.compactMap { day in
calendar.date(byAdding: .day, value: day - 1, to: startDate)
}
}
🤖 Prompt for AI Agents
In `@Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/Date`+.swift around
lines 11 - 17, The getAllDates() implementation force-unwraps
calendar.date(from:) and calendar.range(of:) and uses Date() as a fallback,
which can crash or corrupt calendar results; change getAllDates() to safely
unwrap these calls (use guard let startDate = calendar.date(from:
Calendar.current.dateComponents([.year, .month], from: self)) and guard let
range = calendar.range(of: .day, in: .month, for: startDate)) and return an
empty array (or propagate an error) if they fail, and remove the Date() fallback
inside the compactMap so you only include non-nil results from
calendar.date(byAdding: .day, value: day - 1, to: startDate).

Comment on lines 11 to 24
func dateFormatter() -> String {
let inputFormatter = DateFormatter()
inputFormatter.dateFormat = "yyyy-MM-dd"
inputFormatter.locale = Locale(identifier: "en_US_POSIX")

guard let date = inputFormatter.date(from: self) else {
return self
}

let outputFormatter = DateFormatter()
outputFormatter.dateFormat = "M월 d일 EEEE"
outputFormatter.locale = Locale(identifier: "ko_KR")

return outputFormatter.string(from: date)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

DateFormatter 캐시 및 timeZone 명시가 필요합니다.

매 호출마다 DateFormatter를 생성하면 비용이 큽니다. 또한 입력 날짜가 서버 기준(KST/UTC)이라면 로컬 TZ 파싱으로 날짜가 어긋날 수 있어 timeZone을 명시하는 게 안전합니다.

💡 제안 수정안
 extension String {
+    private static let inputDateFormatter: DateFormatter = {
+        let f = DateFormatter()
+        f.dateFormat = "yyyy-MM-dd"
+        f.locale = Locale(identifier: "en_US_POSIX")
+        f.timeZone = TimeZone(identifier: "Asia/Seoul") // 또는 UTC 등 서버 기준에 맞게 설정
+        return f
+    }()
+    
+    private static let outputDateFormatter: DateFormatter = {
+        let f = DateFormatter()
+        f.dateFormat = "M월 d일 EEEE"
+        f.locale = Locale(identifier: "ko_KR")
+        f.timeZone = TimeZone(identifier: "Asia/Seoul") // 화면 표기 기준에 맞게 설정
+        return f
+    }()
+
     func dateFormatter() -> String {
-        let inputFormatter = DateFormatter()
-        inputFormatter.dateFormat = "yyyy-MM-dd"
-        inputFormatter.locale = Locale(identifier: "en_US_POSIX")
-        
-        guard let date = inputFormatter.date(from: self) else {
+        guard let date = Self.inputDateFormatter.date(from: self) else {
             return self
         }
-        
-        let outputFormatter = DateFormatter()
-        outputFormatter.dateFormat = "M월 d일 EEEE"
-        outputFormatter.locale = Locale(identifier: "ko_KR")
-        
-        return outputFormatter.string(from: date)
+        return Self.outputDateFormatter.string(from: date)
     }
 }
🤖 Prompt for AI Agents
In `@Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/String`+.swift around
lines 11 - 24, The dateFormatter() implementation creates new DateFormatter
instances on each call and omits explicit timeZone handling; change it to use
cached/static DateFormatter instances (e.g., static let inputFormatter and
static let outputFormatter) to avoid per-call allocation and set explicit
timeZone values on both formatters (for example inputFormatter.timeZone =
TimeZone(secondsFromGMT: 0) or server's TZ and outputFormatter.timeZone =
TimeZone(identifier: "Asia/Seoul")) while keeping the existing locale settings;
update the function to parse with the cached inputFormatter and format with the
cached outputFormatter (referencing the symbols dateFormatter(), inputFormatter,
outputFormatter, DateFormatter, locale, timeZone).

Comment on lines +24 to +35
guard let fetchProcedureCountOfMonthUseCase = DIContainer.shared.resolve(type: FetchProcedureCountOfMonth.self),
let fetchTodayProcedureListUseCase = DIContainer.shared.resolve(type: FetchTodayProcedureList.self)
else {
CherrishLogger.error(CherrishError.DIFailedError)
return
}

DIContainer.shared.register(type: CalendarViewModel.self) {
return CalendarViewModel(
fetchProcedureCountOfMonthUseCase: fetchProcedureCountOfMonthUseCase,
fetchTodayProcedureListUseCase: fetchTodayProcedureListUseCase
)
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

등록 시점 캡처 대신 lazy resolve로 안정성 확보를 권장합니다.

assemble 시점에 인스턴스를 캡처하면 이후 DI 스코프 변경(로그아웃/재조립 등) 시 stale 의존성이 남을 수 있습니다. 등록 클로저 내부에서 resolve하도록 바꾸는 편이 안전합니다.

♻️ 제안 수정안
-        guard let fetchProcedureCountOfMonthUseCase = DIContainer.shared.resolve(type: FetchProcedureCountOfMonth.self),
-            let fetchTodayProcedureListUseCase = DIContainer.shared.resolve(type: FetchTodayProcedureList.self)
-        else {
-            CherrishLogger.error(CherrishError.DIFailedError)
-            return
-        }
-        
         DIContainer.shared.register(type: CalendarViewModel.self) {
+            guard let fetchProcedureCountOfMonthUseCase = DIContainer.shared.resolve(type: FetchProcedureCountOfMonth.self),
+                  let fetchTodayProcedureListUseCase = DIContainer.shared.resolve(type: FetchTodayProcedureList.self)
+            else {
+                CherrishLogger.error(CherrishError.DIFailedError)
+                fatalError("CalendarViewModel DI resolve failed")
+            }
             return CalendarViewModel(
                 fetchProcedureCountOfMonthUseCase: fetchProcedureCountOfMonthUseCase,
                 fetchTodayProcedureListUseCase: fetchTodayProcedureListUseCase
             )
         }
🤖 Prompt for AI Agents
In `@Cherrish-iOS/Cherrish-iOS/Presentation/PresentationDependencyAssembler.swift`
around lines 24 - 35, In assemble(), avoid capturing
fetchProcedureCountOfMonthUseCase and fetchTodayProcedureListUseCase at
registration time; instead inside DIContainer.shared.register(type:
CalendarViewModel.self) closure call DIContainer.shared.resolve(...) for
FetchProcedureCountOfMonth and FetchTodayProcedureList and pass those resolved
instances into CalendarViewModel(initializer) so the registration uses lazy
resolution and won't hold stale dependencies across reassembly or scope changes.

Comment on lines +45 to +48
guard let viewModel = DIContainer.shared.resolve(type: CalendarViewModel.self) else {
fatalError()
}
return CalendarView(viewModel: viewModel)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

DI 해상 실패 시 원인 파악이 어렵습니다.

무메시지 fatalError()는 크래시 원인 추적이 어렵습니다. 최소한 로그/메시지를 넣어 디버깅 가능성을 높여주세요.

💡 제안 수정안
-        guard let viewModel = DIContainer.shared.resolve(type: CalendarViewModel.self) else {
-            fatalError()
-        }
+        guard let viewModel = DIContainer.shared.resolve(type: CalendarViewModel.self) else {
+            CherrishLogger.error(CherrishError.DIFailedError)
+            fatalError("CalendarViewModel DI resolve failed")
+        }
🤖 Prompt for AI Agents
In `@Cherrish-iOS/Cherrish-iOS/Presentation/ViewFactory.swift` around lines 45 -
48, The guard that resolves CalendarViewModel currently calls bare fatalError(),
making crashes hard to diagnose; update the failure path in ViewFactory where
DIContainer.shared.resolve(type: CalendarViewModel.self) is unwrapped so it logs
or calls fatalError with a descriptive message (e.g. "Failed to resolve
CalendarViewModel via DIContainer.shared.resolve") and include contextual info
like the view name (CalendarView) or container state; target the guard around
CalendarViewModel resolution and the return CalendarView(viewModel:) to add the
informative message.

Copy link
Contributor

@wotjs020708 wotjs020708 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

🤖 Fix all issues with AI agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarView.swift`:
- Around line 285-287: The use of the deprecated single-parameter onChange
closure needs updating: change the onChange call that observes proxy.frame(in:
.global).minY to the iOS17 API signature so it accepts both old and new values
(e.g., use { _, newValue in topGlobalY = newValue }) or switch to the
no-argument form and read proxy.frame(in: .global).minY inside the body; update
the closure where onChange(of: proxy.frame(in: .global).minY) is used so
topGlobalY is assigned from the newValue (or from proxy.frame(in: .global).minY
if you choose the no-argument variant).

In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/ProcedureView.swift`:
- Around line 64-71: In ProcedureView’s conditional that checks "if status ==
.active" (the block that renders TypographyText for downTimeDays), conform to
the Swift style guide by placing the else keyword on the same line as the
closing brace of the if block (i.e., change the current multi-line "} else {"
placement to "} else {" on one line for the else that handles the non-zero
downTimeDays case).

In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarViewModel.swift`:
- Around line 93-99: fetchDowntimeByDay mutates the `@Published` property
downtimeByDay from a non-main-actor context which can cause UI-thread races;
make the update main-thread-safe by marking the function (or its caller) with
`@MainActor` or by ensuring the mutation happens on the main actor before calling
mapToDowntimeDays; specifically update the fetchDowntimeByDay declaration to be
`@MainActor` (or wrap the downtimeByDay assignments inside Task { `@MainActor` in
... }) so that any access to downtimeByDay and calls to mapToDowntimeDays
execute on the main actor.
♻️ Duplicate comments (3)
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/String+.swift (1)

11-23: timeZone 명시가 여전히 필요합니다.

DateFormatter 캐싱은 잘 반영되었습니다. 그러나 입력 날짜가 서버 기준(KST/UTC)이라면 로컬 타임존 파싱으로 날짜가 어긋날 수 있어 timeZone을 명시하는 것이 안전합니다.

♻️ 제안 수정
 private static let inputFormatter: DateFormatter = {
     let dateFormatter = DateFormatter()
     dateFormatter.dateFormat = "yyyy-MM-dd"
     dateFormatter.locale = Locale(identifier: "en_US_POSIX")
+    dateFormatter.timeZone = TimeZone(identifier: "Asia/Seoul")
     return dateFormatter
 }()
 
 private static let outputFormatter: DateFormatter = {
     let dateFormatter = DateFormatter()
     dateFormatter.dateFormat = "M월 d일 EEEE"
     dateFormatter.locale = Locale(identifier: "ko_KR")
+    dateFormatter.timeZone = TimeZone(identifier: "Asia/Seoul")
     return dateFormatter
 }()
Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarView.swift (1)

27-30: 중복된 nil 초기화 제거 필요

Swift에서 Optional 변수는 기본적으로 nil로 초기화되므로 명시적 = nil은 불필요합니다.

♻️ 제안하는 수정
-    `@State` private var initialTopGlobalY: CGFloat? = nil
+    `@State` private var initialTopGlobalY: CGFloat?
     `@State` private var bottomOffsetY: CGFloat = .zero
     `@State` private var calendarMode: CalendarMode = .none
-    `@State` private var selectedProcedureID: Int? = nil
+    `@State` private var selectedProcedureID: Int?
Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarViewModel.swift (1)

117-133: 변수 섀도잉(Variable Shadowing) 주의

Line 120에서 로컬 변수 currentMonth가 파라미터 currentMonth를 섀도잉합니다. 가독성을 위해 변수명을 변경하는 것이 좋습니다.

♻️ 변수명 변경 제안
     private func extractDate(currentMonth: Int) -> [DateValue] {
         let calendar = Calendar.current
         
-        let currentMonth = getCurrentMonth(addingMonth: currentMonth)
-        var days = currentMonth.getAllDates().compactMap { date -> DateValue in
+        let targetMonth = getCurrentMonth(addingMonth: currentMonth)
+        var days = targetMonth.getAllDates().compactMap { date -> DateValue in
             let day = calendar.component(.day, from: date)
             return DateValue(day: day, date: date)
         }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3e18904 and 3a97630.

📒 Files selected for processing (7)
  • Cherrish-iOS/Cherrish-iOS/Domain/UseCase/FetchProcedureCountOfMonth.swift
  • Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarCellView.swift
  • Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarView.swift
  • Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/ProcedureView.swift
  • Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarViewModel.swift
  • Cherrish-iOS/Cherrish-iOS/Presentation/Global/Components/PreferenceKey.swift
  • Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/String+.swift
🧰 Additional context used
🧬 Code graph analysis (3)
Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/ProcedureView.swift (3)
Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarViewModel.swift (1)
  • isSelected (39-41)
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/String+.swift (1)
  • dateFormatter (25-31)
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/View+Color.swift (7)
  • gray500 (29-31)
  • gray0 (9-11)
  • gray100 (13-15)
  • red600 (85-87)
  • gray300 (21-23)
  • gray900 (45-47)
  • gray800 (41-43)
Cherrish-iOS/Cherrish-iOS/Domain/UseCase/FetchProcedureCountOfMonth.swift (1)
Cherrish-iOS/Cherrish-iOS/Data/Repository/CalendarRepository.swift (2)
  • fetchProcedureCountOfMonth (11-13)
  • fetchProcedureCountOfMonth (21-29)
Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarViewModel.swift (3)
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/Date+.swift (2)
  • toDateString (20-27)
  • getAllDates (11-18)
Cherrish-iOS/Cherrish-iOS/Domain/UseCase/FetchProcedureCountOfMonth.swift (1)
  • execute (21-23)
Cherrish-iOS/Cherrish-iOS/Core/CherrishLogger.swift (1)
  • debug (131-137)
🪛 SwiftLint (0.57.0)
Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarView.swift

[Warning] 27-27: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)


[Warning] 30-30: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)

🔇 Additional comments (17)
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Components/PreferenceKey.swift (1)

10-22: LGTM! 이전 리뷰 피드백이 잘 반영되었습니다.

reduce 함수에서 += 대신 max(value, nextValue())를 사용하여 스크롤 위치 추적 시 여러 뷰가 값을 전달해도 위치가 왜곡되지 않도록 올바르게 수정되었습니다.

Cherrish-iOS/Cherrish-iOS/Domain/UseCase/FetchProcedureCountOfMonth.swift (1)

10-24: LGTM! 이전 리뷰 피드백이 반영되었습니다.

repopsitory 오타가 repository로 수정되었고, 프로토콜과 구현체가 깔끔하게 구성되어 있습니다. Repository 패턴이 올바르게 적용되었습니다.

Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarCellView.swift (2)

10-71: LGTM! 캘린더 셀 구현이 잘 되어 있습니다.

이전 리뷰의 명시적 return 피드백이 반영되었고, DowntimeDayState enum과 CalendarCellView의 구조가 깔끔합니다. 상태에 따른 UI 렌더링 로직이 명확하게 분리되어 있습니다.


73-112: LGTM! 색상 및 타이틀 매핑이 일관성 있게 구현되었습니다.

DowntimeDayState extension의 모든 computed property에서 명시적 return이 사용되어 코드 스타일이 일관됩니다.

Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/ProcedureView.swift (2)

10-47: LGTM! 이전 리뷰 피드백이 반영되었습니다.

RoundedRectangle(cornerRadius: 6)에서 불필요한 공백이 제거되었고, ProcedureStatus enum을 통한 상태 기반 스타일링이 깔끔하게 구현되어 있습니다.


84-129: LGTM! 상태별 색상 매핑이 잘 구성되어 있습니다.

ProcedureStatus extension을 통해 각 상태에 따른 색상을 명확하게 정의하여 스타일링 로직이 잘 분리되어 있습니다.

Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarView.swift (7)

10-22: LGTM!

CalendarMode enum과 toggle() 메서드가 깔끔하게 구현되어 있습니다.


48-55: LGTM!

.task를 사용한 비동기 데이터 로딩과 에러 처리가 적절합니다.


61-98: LGTM!

월 네비게이션 로직과 요일 헤더 표시가 잘 구현되어 있습니다.


100-130: LGTM!

날짜 그리드 렌더링과 날짜 선택 시 상태 관리가 적절하게 구현되어 있습니다.


169-173: 이전 리뷰 반영 확인

procedure.procedureId를 직접 사용하도록 수정되어 잘못된 ID(0) 전달 문제가 해결되었습니다.


231-238: 버튼 액션이 비어 있음

CherrishButtonaction이 빈 클로저입니다. 시술 일정 추가 기능이 향후 구현 예정인지 확인이 필요합니다.


255-272: 이전 리뷰 반영됨

.allCases 대신 .displayCases를 사용하여 이전 지적 사항이 완벽하게 수정되었습니다. displayCases.none을 제외한 [.sensitive, .caution, .recovery]만 반환하므로 .none 상태는 아이콘으로 표시되지 않습니다.

Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarViewModel.swift (4)

10-14: LGTM!

DateValue 구조체가 IdentifiableHashable을 적절히 준수하고 있습니다.


16-33: LGTM!

의존성 주입 패턴과 @Published 속성 구성이 적절합니다.


60-66: 이전 리뷰 반영 확인

강제 언래핑(!) 대신 ?? targetDate 폴백을 사용하도록 수정되어 크래시 위험이 제거되었습니다.


135-142: 다운타임 상태 우선순위 확인 필요

sensitiveDayscautionDaysrecoveryDays 순서로 처리되어, 동일 날짜가 여러 배열에 존재하면 recovery가 최종 상태가 됩니다. 이 우선순위가 의도된 것인지 확인이 필요합니다.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +285 to +287
.onChange(of: proxy.frame(in: .global).minY) { v in
topGlobalY = v
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

fd -e pbxproj | xargs grep -i "IPHONEOS_DEPLOYMENT_TARGET" | head -10

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 225


🌐 Web query:

SwiftUI onChange API iOS 17 deprecation single parameter closure

💡 Result:

Short answer: In iOS 17 Apple deprecated the single-parameter onChange(of:perform:) closure. Use the new overloads that take either zero parameters or two parameters (oldValue, newValue). You can also pass initial: Bool to run on appearance. Examples:

  • Zero-parameter (use when you don't need values):
    .onChange(of: value) {
    // read state/Environment directly here
    }

  • Two-parameter (old and new):
    .onChange(of: value, initial: true) { oldValue, newValue in
    // handle change using oldValue and newValue
    }

To convert existing single-arg code that used newValue, discard oldValue with _:
.onChange(of: value) { _, newValue in /* ... */ }

Sources: Apple Developer discussion / docs (onChange overloads). [1] and community Q&A showing migration examples. [2]


🏁 Script executed:

cd Cherrish-iOS && find . -name "CalendarView.swift" -exec head -300 {} \; | tail -50

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 1757


iOS 17+ onChange API 업데이트 필요

현재 코드의 onChange(of:) 클로저가 단일 파라미터 형식을 사용하고 있습니다. 이 형식은 iOS 17부터 deprecated되었습니다. 프로젝트의 최소 지원 버전이 iOS 16.6이므로 기존 형식도 작동하지만, iOS 17 이상에서는 새로운 API를 사용해야 합니다.

다음과 같이 수정하세요:

.onChange(of: proxy.frame(in: .global).minY) { _, newValue in
    topGlobalY = newValue
}

또는 값을 사용하지 않으면:

.onChange(of: proxy.frame(in: .global).minY) {
    topGlobalY = proxy.frame(in: .global).minY
}
🤖 Prompt for AI Agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarView.swift`
around lines 285 - 287, The use of the deprecated single-parameter onChange
closure needs updating: change the onChange call that observes proxy.frame(in:
.global).minY to the iOS17 API signature so it accepts both old and new values
(e.g., use { _, newValue in topGlobalY = newValue }) or switch to the
no-argument form and read proxy.frame(in: .global).minY inside the body; update
the closure where onChange(of: proxy.frame(in: .global).minY) is used so
topGlobalY is assigned from the newValue (or from proxy.frame(in: .global).minY
if you choose the no-argument variant).

Comment on lines +93 to +99
func fetchDowntimeByDay(procedureId: Int) {
guard let procedure = procedureList.first(where: { $0.procedureId == procedureId }) else {
downtimeByDay = [:]
return
}
mapToDowntimeDays(procedure: procedure)
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

스레드 안전성 주의

fetchDowntimeByDay@MainActor가 없지만 @Published 속성인 downtimeByDay를 수정합니다. 현재 UI에서 동기적으로 호출되고 있어 문제가 없지만, 향후 백그라운드에서 호출될 경우 UI 업데이트 문제가 발생할 수 있습니다.

♻️ 제안하는 수정
+    `@MainActor`
     func fetchDowntimeByDay(procedureId: Int) {
         guard let procedure = procedureList.first(where: { $0.procedureId == procedureId }) else {
             downtimeByDay = [:]
             return
         }
         mapToDowntimeDays(procedure: procedure)
     }
🤖 Prompt for AI Agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarViewModel.swift`
around lines 93 - 99, fetchDowntimeByDay mutates the `@Published` property
downtimeByDay from a non-main-actor context which can cause UI-thread races;
make the update main-thread-safe by marking the function (or its caller) with
`@MainActor` or by ensuring the mutation happens on the main actor before calling
mapToDowntimeDays; specifically update the fetchDowntimeByDay declaration to be
`@MainActor` (or wrap the downtimeByDay assignments inside Task { `@MainActor` in
... }) so that any access to downtimeByDay and calls to mapToDowntimeDays
execute on the main actor.

Copy link
Contributor

@sum130 sum130 left a comment

Choose a reason for hiding this comment

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

캘린더 어려웠을텐데 짧은 시간에 구현하느라 너무너무 고생많았어요!!!
usecase랑 뷰모델 작성할 때 또 다시 보러올게요ㅎㅎ

KakaoTalk_Photo_2026-01-09-23-22-18 009

Comment on lines +285 to +287
.onChange(of: proxy.frame(in: .global).minY) { v in
topGlobalY = v
}
Copy link
Contributor

Choose a reason for hiding this comment

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

버전 높이기! 리마인드 드립니당

}
}
}
.padding(.trailing, 10)
Copy link
Contributor

Choose a reason for hiding this comment

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

adjustedW 적용하면 좋을 것 같아요~~

@Published private(set) var currentDate: Date = Date()
@Published var currentMonth: Int = 0
@Published var selectedDate: Date = Calendar.current.startOfDay(for: Date())
@Published private(set) var procedureCountOfMonth: [Int: Int] = [:]
Copy link
Contributor

Choose a reason for hiding this comment

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

private(set) 처음 봤어요 덕분에 공부하고 갑니다!!
다른 분들을 위해.. 해당 키워드는 '외부에서 읽기만 가능하고 내부에서만 수정이 가능하도록' 하는 기능입니다!! 라고 적어두고갈게용

Copy link
Contributor

Choose a reason for hiding this comment

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

감사합니당

Comment on lines +126 to +142
private func extractDate(currentMonth: Int) -> [DateValue] {
let calendar = Calendar.current

let currentMonth = getCurrentMonth(addingMonth: currentMonth)
var days = currentMonth.getAllDates().compactMap { date -> DateValue in
let day = calendar.component(.day, from: date)
return DateValue(day: day, date: date)
}

let firstWeekday = calendar.component(.weekday, from: days.first?.date ?? Date())

for _ in 0 ..< firstWeekday - 1 {
days.insert(DateValue(day: -1, date: Date()), at: 0)
}

return days
}
Copy link
Contributor

Choose a reason for hiding this comment

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

한 번 확인하면 좋을 것 같아요~

Comment on lines 26 to 28
var state: ButtonState
let leadingIcon: Image?
let trailingIcon: Image?
Copy link
Contributor

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

🤖 Fix all issues with AI agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarView.swift`:
- Around line 170-173: 현재 .onTapGesture에서 calendarMode.toggle() 사용으로 다른 항목을 탭했을
때 calendarMode가 .none으로 바뀌어 선택 변경을 위해 2탭이 필요해지는 문제입니다; 수정 방법은 toggle을 제거하고 탭한
항목이 기존 선택과 같은지 검사해 같으면 calendarMode = .none으로 토글(즉 해제)하고 다르면 calendarMode =
.selectedProcedure로 명시적으로 설정하도록 변경하는 것입니다 — 관련 심볼: calendarMode,
selectedProcedureID, procedure.procedureId, viewModel.fetchDowntimeByDay,
.onTapGesture.
♻️ Duplicate comments (4)
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Components/CherrishButton.swift (2)

27-28: optional 프로퍼티에 기본값 누락

leadingIcontrailingIcon은 optional이지만 기본값이 없어서 기존 호출부에서 컴파일 에러가 발생할 수 있습니다. 커스텀 이니셜라이저를 추가하여 기본값을 제공하세요.

🐛 커스텀 이니셜라이저 추가 제안
 struct CherrishButton: View {
     
     let title: String
     let type: CherrishButtonType
     `@Binding` var state: ButtonState
     let leadingIcon: Image?
     let trailingIcon: Image? 
     let action: () -> Void
+    
+    init(
+        title: String,
+        type: CherrishButtonType,
+        state: Binding<ButtonState>,
+        leadingIcon: Image? = nil,
+        trailingIcon: Image? = nil,
+        action: `@escaping` () -> Void
+    ) {
+        self.title = title
+        self.type = type
+        self._state = state
+        self.leadingIcon = leadingIcon
+        self.trailingIcon = trailingIcon
+        self.action = action
+    }

33-48: 아이콘과 텍스트 간 간격 및 색상 처리 필요

두 가지 문제가 있습니다:

  1. 간격 문제: HStack(spacing: 0)으로 설정되어 아이콘과 텍스트가 간격 없이 붙어 렌더링됩니다.

  2. 아이콘 색상 누락: 아이콘에 버튼 상태에 따른 색상이 적용되지 않아 텍스트와 일관성이 떨어집니다.

♻️ 간격 및 색상 적용 제안
-            HStack(spacing: 0){
+            HStack(spacing: 8){
                 Spacer()
                 
                 if let leadingIcon {
                     leadingIcon
+                        .renderingMode(.template)
+                        .foregroundStyle(type.textColor(for: state))
                         .frame(width: 24, height: 24)
                 }
                 
                 Text(title)
                     .typography(.title2_sb_16)
                     .foregroundStyle(type.textColor(for: state))
                 
                 if let trailingIcon {
                     trailingIcon
+                        .renderingMode(.template)
+                        .foregroundStyle(type.textColor(for: state))
                         .frame(width: 24, height: 24)
                 }
                 
                 Spacer()
             }
Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarView.swift (2)

24-31: 옵셔널 @State의 명시적 nil 초기화는 불필요합니다.

Swift에서 옵셔널은 기본값이 nil이므로 중복입니다.

♻️ 제안하는 수정
-    `@State` private var initialTopGlobalY: CGFloat? = nil
+    `@State` private var initialTopGlobalY: CGFloat?
 ...
-    `@State` private var selectedProcedureID: Int? = nil
+    `@State` private var selectedProcedureID: Int?

277-288: iOS 17+에서 단일 파라미터 onChange가 deprecated입니다.

새 시그니처로 변경을 권장합니다.

♻️ 제안하는 수정
-                .onChange(of: proxy.frame(in: .global).minY) { v in
-                    topGlobalY = v
-                }
+                .onChange(of: proxy.frame(in: .global).minY) { _, newValue in
+                    topGlobalY = newValue
+                }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3a97630 and b3473f9.

📒 Files selected for processing (2)
  • Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarView.swift
  • Cherrish-iOS/Cherrish-iOS/Presentation/Global/Components/CherrishButton.swift
🧰 Additional context used
🧬 Code graph analysis (1)
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Components/CherrishButton.swift (3)
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/View+Shadow.swift (1)
  • body (9-17)
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/Font+.swift (1)
  • typography (60-65)
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/View+Color.swift (4)
  • red700 (89-91)
  • gray200 (17-19)
  • gray0 (9-11)
  • gray600 (33-35)
🪛 SwiftLint (0.57.0)
Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarView.swift

[Warning] 27-27: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)


[Warning] 30-30: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)

🔇 Additional comments (9)
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Components/CherrishButton.swift (2)

15-20: LGTM!

버튼 타입 네이밍이 기능 기반(next, confirm, save)에서 크기 기반(large, small, medium)으로 변경되어 재사용성이 향상되었습니다.


74-121: LGTM!

버튼 타입별 스타일 매핑이 일관성 있게 구현되었습니다. .large는 상태 기반 스타일링, .small/.medium은 고정 스타일을 적용하는 구조가 명확합니다.

Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarView.swift (7)

10-21: CalendarMode 토글 로직이 간결합니다.

현 상태 전환이 명확하고 가독성이 좋습니다.


38-58: 초기 데이터 로딩 흐름이 명확합니다.

task에서 월별 카운트와 오늘 리스트를 순차 로딩하는 구조가 좋습니다.


61-99: 헤더와 요일 렌더링 구성이 안정적입니다.

구성 및 레이아웃 흐름에 문제 없어 보입니다.


101-131: 날짜 선택 및 비동기 갱신 처리 OK.

선택 시 상태 초기화와 재조회 흐름이 자연스럽습니다.


217-254: 빈 상태 UI 구성이 깔끔합니다.

레이아웃과 버튼 구성 모두 문제 없어 보입니다.


256-273: 다운타임 범례 렌더링이 명확합니다.

displayCases 사용도 적절합니다.


293-313: 그라데이션 표시 조건 로직이 합리적입니다.

상태 제어가 명확하게 분리돼 있습니다.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +170 to +173
.onTapGesture {
calendarMode.toggle()
selectedProcedureID = procedure.procedureId
viewModel.fetchDowntimeByDay(procedureId: procedure.procedureId)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

절차 선택 시 토글로 인해 다른 항목 선택이 2탭 필요해질 수 있습니다.

현재는 선택 상태에서 다른 항목을 탭해도 모드가 .none으로 전환되어 즉시 선택 상태를 유지하지 못합니다. 항목 변경 시에는 .selectedProcedure를 유지하도록 분기하는 편이 자연스럽습니다.

🐛 제안하는 수정
-                        .onTapGesture {
-                            calendarMode.toggle()
-                            selectedProcedureID = procedure.procedureId
-                            viewModel.fetchDowntimeByDay(procedureId: procedure.procedureId)
-                        }
+                        .onTapGesture {
+                            let isSame = selectedProcedureID == procedure.procedureId
+                            calendarMode = isSame ? .none : .selectedProcedure
+                            selectedProcedureID = isSame ? nil : procedure.procedureId
+                            if !isSame {
+                                viewModel.fetchDowntimeByDay(procedureId: procedure.procedureId)
+                            }
+                        }
🤖 Prompt for AI Agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarView.swift`
around lines 170 - 173, 현재 .onTapGesture에서 calendarMode.toggle() 사용으로 다른 항목을 탭했을
때 calendarMode가 .none으로 바뀌어 선택 변경을 위해 2탭이 필요해지는 문제입니다; 수정 방법은 toggle을 제거하고 탭한
항목이 기존 선택과 같은지 검사해 같으면 calendarMode = .none으로 토글(즉 해제)하고 다르면 calendarMode =
.selectedProcedure로 명시적으로 설정하도록 변경하는 것입니다 — 관련 심볼: calendarMode,
selectedProcedureID, procedure.procedureId, viewModel.fetchDowntimeByDay,
.onTapGesture.

Copy link
Contributor

@soseoyo12 soseoyo12 left a comment

Choose a reason for hiding this comment

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

캘린더 복잡해보이는데 구현하느라 수고하셨습니다 ㅜ
image

@y-eonee y-eonee force-pushed the style/#39-캘린더 branch from b3473f9 to 45a3b36 Compare January 18, 2026 14:42
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: 7

🤖 Fix all issues with AI agents
In `@Cherrish-iOS/Cherrish-iOS/Domain/DomainDependencyAssembler.swift`:
- Around line 24-26: 현재 assemble()에서 calendarRepository를 미리 resolve해 클로저에 캡처하고
있어 DI 재조립 시 stale 인스턴스를 참조할 수 있습니다; DIContainer.shared.register(...) 호출에서
FetchProcedureCountOfMonth/DefaultFetchProcedureCountOfMonth 등록 시 클로저 내부에서
calendarRepository를 직접 resolve 하도록 변경하여 lazy resolve 패턴을 적용하고 동일한 패턴을 36-38번지의
다른 등록들에도 적용하세요 (참조 심볼: assemble(), DIContainer.shared.register,
calendarRepository, FetchProcedureCountOfMonth,
DefaultFetchProcedureCountOfMonth).

In `@Cherrish-iOS/Cherrish-iOS/Domain/Interface/CalendarInterface.swift`:
- Around line 10-13: The CalendarInterface methods are synchronous but the use
cases FetchProcedureCountOfMonth and FetchTodayProcedureList are declared async
throws; update the API to be consistent: either change CalendarInterface's
method signatures fetchProcedureCountOfMonth(year:month:) and
fetchTodayProcedureList(date:) to async throws if they may perform network/async
work (propagate errors) or change the use case declarations to synchronous
non-throwing variants to match the current implementation; pick the async throws
route if fetching can fail or is asynchronous, and ensure all conforming
implementations and callers (use case types) are updated accordingly.

In `@Cherrish-iOS/Cherrish-iOS/Domain/UseCase/FetchProcedureCountOfMonth.swift`:
- Around line 10-24: The execute signature on FetchProcedureCountOfMonth (and
DefaultFetchProcedureCountOfMonth.execute) is declared as async throws but calls
a synchronous, non-throwing repository method
repository.fetchProcedureCountOfMonth(year:month:); fix the mismatch by choosing
one approach: either (A) update CalendarInterface.fetchProcedureCountOfMonth to
be async throws (and propagate that change to all implementations and other use
cases like FetchTodayProcedureList), or (B) change
FetchProcedureCountOfMonth.execute and DefaultFetchProcedureCountOfMonth.execute
to be synchronous non-throwing (remove async throws) to match the current
repository; locate the protocol FetchProcedureCountOfMonth, struct
DefaultFetchProcedureCountOfMonth, and the CalendarInterface method to apply the
chosen change consistently.

In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarView.swift`:
- Around line 65-85: When changing months in the calendar (the
Image(.chevronLeft) and Image(.chevronRight) onTapGesture handlers), you're only
updating viewModel.currentMonth and viewModel.selectedDate via
viewModel.firstDateOfCurrentMonth(), but not reloading data; call the viewModel
methods that refresh the displayed data—specifically invoke
fetchProcedureCountsOfMonth() and fetchTodayProcedureList() (or the existing
viewModel refresh method) immediately after updating currentMonth and
selectedDate so the counts and list reflect the newly selected month.

In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarViewModel.swift`:
- Around line 10-14: DateValue's id is unstable because it uses UUID() causing
cell churn each time getDatesArray() recreates values; change id to be
deterministically derived from the date (for example make id a computed property
based on date like date.timeIntervalSince1970 or an ISO8601/yyyymmdd formatted
string) so Identifiable stability is preserved; update the DateValue struct (and
any places constructing it) to remove the random UUID and use the date-derived
id (keep day and date fields unchanged) so SwiftUI diffs are stable.

In `@Cherrish-iOS/Cherrish-iOS/Presentation/Global/Components/GradientBox.swift`:
- Around line 10-19: Add a SwiftUI preview for the GradientBox view so reviewers
and designers can see both states; create a PreviewProvider referencing
GradientBox and supply examples for isTop = true and isTop = false (e.g., in a
VStack or ForEach), include a fixed frame or .previewLayout(.sizeThatFits) and
any background contrast so the gradient is visible, and ensure the preview uses
the same struct name GradientBox and its isTop property.

In `@Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/Date`+.swift:
- Around line 20-27: 현재 toDateString()에서 매 호출마다 DateFormatter를 생성해 성능 저하가 발생할 수
있으니 DateFormatter를 캐싱하도록 변경하세요: Date 확장에 static 프로퍼티(e.g. yyyyMMddFormatter)를
추가해 locale, timeZone, dateFormat을 한 번만 설정하고, toDateString()는 새 포매터를 만들지 말고 해당
static 프로퍼티(예: Date.yyyyMMddFormatter)를 사용해 문자열을 반환하도록 바꿉니다; 필요하면 멀티스레드 환경에서의
안전성을 고려해 호출 위치에서 동기화 또는 별도 포매터 사용 방안을 검토하세요.
♻️ Duplicate comments (14)
Cherrish-iOS/Cherrish-iOS/Assets.xcassets/Color/gray_0_0.colorset/Contents.json (1)

1-38: 이전 리뷰에서 이미 지적된 문제입니다.

이 색상 에셋의 문제점(명명 규칙 위반, alpha 0.000으로 인한 완전 투명, 미사용)은 이전 리뷰에서 상세히 지적되었습니다. 해당 피드백을 검토하고 조치해 주세요.

Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/Date+.swift (1)

11-18: Force unwrap 및 Date() 폴백 사용에 대한 위험성.

이 코드 세그먼트의 문제점은 이전 리뷰에서 이미 지적되었습니다. calendar.date(from:)calendar.range(of:)의 강제 언래핑은 크래시 위험이 있고, Date() 폴백은 잘못된 데이터를 반환할 수 있습니다. 이전 리뷰의 제안된 수정 사항을 적용해 주세요.

Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/String+.swift (1)

11-23: timeZone 명시가 필요합니다.

DateFormatter 캐싱은 잘 적용되었으나, timeZone이 여전히 명시되지 않았습니다. 서버 기준(UTC/KST)과 로컬 타임존이 다를 경우 날짜가 어긋날 수 있습니다.

💡 제안 수정안
     private static let inputFormatter: DateFormatter = {
         let dateFormatter = DateFormatter()
         dateFormatter.dateFormat = "yyyy-MM-dd"
         dateFormatter.locale = Locale(identifier: "en_US_POSIX")
+        dateFormatter.timeZone = TimeZone(identifier: "Asia/Seoul") // 또는 서버 기준에 맞게 설정
         return dateFormatter
     }()
     
     private static let outputFormatter: DateFormatter = {
         let dateFormatter = DateFormatter()
         dateFormatter.dateFormat = "M월 d일 EEEE"
         dateFormatter.locale = Locale(identifier: "ko_KR")
+        dateFormatter.timeZone = TimeZone(identifier: "Asia/Seoul")
         return dateFormatter
     }()
Cherrish-iOS/Cherrish-iOS/Data/Model/CalendarDailyResponseDTO.swift (1)

27-30: toEntity() 구현이 비어있어 변환이 누락됩니다.

현재 toEntity()는 반환 타입도 없고 구현도 없습니다. 컴파일은 되지만 실제 DTO → Entity 변환이 수행되지 않습니다. 실제 매핑 로직을 구현하거나, 아직 미사용이라면 fatalError("Not implemented")로 조기 탐지를 권장합니다.

💡 제안 수정안
 extension CalendarDailyResponseDTO {
-    func toEntity() {
-        
-    }
+    func toEntity() -> [ProcedureEntity] {
+        return events.map { event in
+            ProcedureEntity(
+                procedureId: event.procedureId,
+                name: event.name,
+                downtimeDays: event.downtimeDays,
+                sensitiveDays: event.sensitiveDays,
+                cautionDays: event.cautionDays,
+                recoveryDays: event.recoveryDays
+            )
+        }
+    }
 }
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Components/CherrishButton.swift (2)

27-28: optional 프로퍼티에 기본값이 누락되었습니다.

leadingIcontrailingIcon은 optional이지만 기본값이 없어서, 아이콘 없이 버튼을 생성하는 기존 호출부에서 컴파일 에러가 발생할 수 있습니다.

💡 커스텀 이니셜라이저 추가 제안
 struct CherrishButton: View {
     
     let title: String
     let type: CherrishButtonType
     `@Binding` var state: ButtonState
     let leadingIcon: Image?
     let trailingIcon: Image? 
     let action: () -> Void
+    
+    init(
+        title: String,
+        type: CherrishButtonType,
+        state: Binding<ButtonState>,
+        leadingIcon: Image? = nil,
+        trailingIcon: Image? = nil,
+        action: `@escaping` () -> Void
+    ) {
+        self.title = title
+        self.type = type
+        self._state = state
+        self.leadingIcon = leadingIcon
+        self.trailingIcon = trailingIcon
+        self.action = action
+    }

36-48: 아이콘에 색상 modifier가 누락되었습니다.

아이콘이 버튼 상태에 따른 색상 변경 없이 원본 색상으로 렌더링됩니다. 텍스트와 동일한 색상을 적용해야 일관된 UI를 제공할 수 있습니다.

♻️ 아이콘 색상 적용 제안
                 if let leadingIcon {
                     leadingIcon
+                        .renderingMode(.template)
+                        .foregroundStyle(type.textColor(for: state))
                         .frame(width: 24, height: 24)
                 }
                 
                 Text(title)
                     .typography(.title2_sb_16)
                     .foregroundStyle(type.textColor(for: state))
                 
                 if let trailingIcon {
                     trailingIcon
+                        .renderingMode(.template)
+                        .foregroundStyle(type.textColor(for: state))
                         .frame(width: 24, height: 24)
                 }
Cherrish-iOS/Cherrish-iOS/Presentation/ViewFactory.swift (1)

46-50: DI 해상 실패 시 원인 파악이 어렵습니다.

무메시지 fatalError()는 크래시 원인 추적이 어렵습니다. 최소한 로그/메시지를 넣어 디버깅 가능성을 높여주세요.

💡 제안 수정안
     func makeCalendarView() -> CalendarView {
         guard let viewModel = DIContainer.shared.resolve(type: CalendarViewModel.self) else {
-            fatalError()
+            fatalError("CalendarViewModel DI resolve failed")
         }
         return CalendarView(viewModel: viewModel)
     }
Cherrish-iOS/Cherrish-iOS/Presentation/PresentationDependencyAssembler.swift (1)

24-36: assemble 시점에 캡처된 의존성은 DI 스코프 변경 시 stale 상태가 될 수 있습니다.

현재 fetchProcedureCountOfMonthUseCasefetchTodayProcedureListUseCaseregister 클로저 외부에서 resolve되어 캡처됩니다. 로그아웃/재조립 시 stale 의존성이 남을 수 있으므로, 클로저 내부에서 lazy resolve하는 것이 안전합니다.

♻️ 권장 수정안
-        guard let fetchProcedureCountOfMonthUseCase = DIContainer.shared.resolve(type: FetchProcedureCountOfMonth.self),
-            let fetchTodayProcedureListUseCase = DIContainer.shared.resolve(type: FetchTodayProcedureList.self)
-        else {
-            CherrishLogger.error(CherrishError.DIFailedError)
-            return
-        }
-        
         DIContainer.shared.register(type: CalendarViewModel.self) {
+            guard let fetchProcedureCountOfMonthUseCase = DIContainer.shared.resolve(type: FetchProcedureCountOfMonth.self),
+                  let fetchTodayProcedureListUseCase = DIContainer.shared.resolve(type: FetchTodayProcedureList.self)
+            else {
+                CherrishLogger.error(CherrishError.DIFailedError)
+                fatalError("CalendarViewModel DI resolve failed")
+            }
             return CalendarViewModel(
                 fetchProcedureCountOfMonthUseCase: fetchProcedureCountOfMonthUseCase,
                 fetchTodayProcedureListUseCase: fetchTodayProcedureListUseCase
             )
         }
Cherrish-iOS/Cherrish-iOS/Domain/UseCase/FetchTodayProcedureList.swift (1)

10-24: async throws 시그니처가 동기 레포지토리 호출과 불일치합니다.

execute(date:)async throws로 선언되어 있지만, repository.fetchTodayProcedureList(date:)는 동기/비에러입니다. 호출부에서 불필요한 await/try를 강제하게 됩니다.

CalendarInterfaceasync throws로 변경하거나, 유스케이스 시그니처를 동기로 맞춰주세요.

♻️ 동기화 선택 시 수정안
 protocol FetchTodayProcedureList {
-    func execute(date: String) async throws -> [ProcedureEntity]
+    func execute(date: String) -> [ProcedureEntity]
 }

 struct DefaultFetchTodayProcedure: FetchTodayProcedureList {
     private let repository: CalendarInterface
     
     init(repository: CalendarInterface) {
         self.repository = repository
     }
     
-    func execute(date: String) async throws -> [ProcedureEntity] {
+    func execute(date: String) -> [ProcedureEntity] {
         repository.fetchTodayProcedureList(date: date)
     }
 }
Cherrish-iOS/Cherrish-iOS/Domain/DomainDependencyAssembler.swift (1)

20-22: CalendarInterface 해결 실패 시 로깅 없이 조용히 반환됩니다.

CalendarInterface resolve 실패 시 로그 없이 반환되어 런타임 디버깅이 어려워집니다.

♻️ 실패 로깅 추가
         guard let calendarRepository = DIContainer.shared.resolve(type: CalendarInterface.self) else {
+            CherrishLogger.error(CherrishError.DIFailedError)
             return
         }
Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarView.swift (3)

26-31: Optional @State의 = nil 초기화는 중복입니다.

Line 27-30에서 Optional을 = nil로 초기화해 SwiftLint 경고가 다시 발생합니다. 기본 nil 초기화를 사용하세요.

♻️ 제안 수정
-    `@State` private var initialTopGlobalY: CGFloat? = nil
+    `@State` private var initialTopGlobalY: CGFloat?
     `@State` private var bottomOffsetY: CGFloat = .zero
     `@State` private var calendarMode: CalendarMode = .none
-    `@State` private var selectedProcedureID: Int? = nil
+    `@State` private var selectedProcedureID: Int?

170-173: 프로시저 선택 전환에서 toggle() 사용으로 다른 항목 선택이 2탭 필요합니다.

Line 170-173에서 calendarMode.toggle()을 사용하면 선택된 상태에서 다른 항목을 탭해도 한 번은 해제되고 다시 탭해야 선택됩니다. 동일 항목 여부로 분기해 모드를 명시적으로 설정하세요.

🐛 제안 수정
                         .onTapGesture {
-                            calendarMode.toggle()
-                            selectedProcedureID = procedure.procedureId
-                            viewModel.fetchDowntimeByDay(procedureId: procedure.procedureId)
+                            let isSame = selectedProcedureID == procedure.procedureId
+                            calendarMode = isSame ? .none : .selectedProcedure
+                            selectedProcedureID = isSame ? nil : procedure.procedureId
+                            if !isSame {
+                                viewModel.fetchDowntimeByDay(procedureId: procedure.procedureId)
+                            }
                         }

286-287: iOS 17에서 단일 파라미터 onChange가 deprecated입니다.

Line 286-287의 단일 파라미터 onChange는 iOS 17에서 deprecated입니다. 새 시그니처로 교체해 경고를 제거하세요.

♻️ 제안 수정
-                .onChange(of: proxy.frame(in: .global).minY) { v in
-                    topGlobalY = v
-                }
+                .onChange(of: proxy.frame(in: .global).minY) { _, newValue in
+                    topGlobalY = newValue
+                }
SwiftUI onChange(of:) deprecated single-parameter closure iOS 17
Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarViewModel.swift (1)

93-99: @published 업데이트를 메인 액터로 보장하세요.

Line 93-99에서 downtimeByDay를 메인 액터 보장 없이 수정합니다. 호출 경로가 바뀌면 UI 스레드 경고가 발생할 수 있으니 @MainActor를 부여하세요.

🐛 제안 수정
-    func fetchDowntimeByDay(procedureId: Int) {
+    `@MainActor`
+    func fetchDowntimeByDay(procedureId: Int) {
         guard let procedure = procedureList.first(where: { $0.procedureId == procedureId }) else {
             downtimeByDay = [:]
             return
         }
         mapToDowntimeDays(procedure: procedure)
     }
Swift `@MainActor` `@Published` thread safety best practice

Comment on lines +24 to 26
DIContainer.shared.register(type: FetchProcedureCountOfMonth.self) {
return DefaultFetchProcedureCountOfMonth(repository: calendarRepository)
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

등록 시점에 캡처된 calendarRepository는 stale 상태가 될 수 있습니다.

calendarRepositoryassemble() 호출 시점에 resolve되어 클로저에 캡처됩니다. DI 컨테이너가 재조립되면 이전 인스턴스가 계속 사용됩니다.

PresentationDependencyAssembler와 동일한 패턴으로, 클로저 내부에서 lazy resolve하는 것이 안전합니다.

♻️ Lazy resolve 패턴 적용
-        guard let calendarRepository = DIContainer.shared.resolve(type: CalendarInterface.self) else {
-            return
-        }
-        
         DIContainer.shared.register(type: FetchProcedureCountOfMonth.self) {
+            guard let calendarRepository = DIContainer.shared.resolve(type: CalendarInterface.self) else {
+                CherrishLogger.error(CherrishError.DIFailedError)
+                fatalError("CalendarInterface DI resolve failed")
+            }
             return DefaultFetchProcedureCountOfMonth(repository: calendarRepository)
         }
         
         // ... homeRepository 처리 ...
     
         DIContainer.shared.register(type: FetchTodayProcedureList.self) {
+            guard let calendarRepository = DIContainer.shared.resolve(type: CalendarInterface.self) else {
+                CherrishLogger.error(CherrishError.DIFailedError)
+                fatalError("CalendarInterface DI resolve failed")
+            }
             return DefaultFetchTodayProcedure(repository: calendarRepository)
         }

Also applies to: 36-38

🤖 Prompt for AI Agents
In `@Cherrish-iOS/Cherrish-iOS/Domain/DomainDependencyAssembler.swift` around
lines 24 - 26, 현재 assemble()에서 calendarRepository를 미리 resolve해 클로저에 캡처하고 있어 DI
재조립 시 stale 인스턴스를 참조할 수 있습니다; DIContainer.shared.register(...) 호출에서
FetchProcedureCountOfMonth/DefaultFetchProcedureCountOfMonth 등록 시 클로저 내부에서
calendarRepository를 직접 resolve 하도록 변경하여 lazy resolve 패턴을 적용하고 동일한 패턴을 36-38번지의
다른 등록들에도 적용하세요 (참조 심볼: assemble(), DIContainer.shared.register,
calendarRepository, FetchProcedureCountOfMonth,
DefaultFetchProcedureCountOfMonth).

Comment on lines +10 to +13
protocol CalendarInterface {
func fetchProcedureCountOfMonth(year: Int, month: Int) -> [Int : Int]
func fetchTodayProcedureList(date: String) -> [ProcedureEntity]
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

인터페이스와 유스케이스 간 async/throws 시그니처 불일치

CalendarInterface의 메서드들은 동기/비에러이지만, 이를 사용하는 FetchProcedureCountOfMonthFetchTodayProcedureList 유스케이스는 async throws로 선언되어 있습니다. 실제 네트워크 호출이 필요한 경우 인터페이스를 async throws로 변경하거나, 현재처럼 동기 구현이면 유스케이스도 동기로 맞춰주세요.

♻️ 비동기 호출이 필요한 경우 인터페이스 수정안
 protocol CalendarInterface {
-    func fetchProcedureCountOfMonth(year: Int, month: Int) -> [Int : Int]
-    func fetchTodayProcedureList(date: String) -> [ProcedureEntity]
+    func fetchProcedureCountOfMonth(year: Int, month: Int) async throws -> [Int : Int]
+    func fetchTodayProcedureList(date: String) async throws -> [ProcedureEntity]
 }
📝 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
protocol CalendarInterface {
func fetchProcedureCountOfMonth(year: Int, month: Int) -> [Int : Int]
func fetchTodayProcedureList(date: String) -> [ProcedureEntity]
}
protocol CalendarInterface {
func fetchProcedureCountOfMonth(year: Int, month: Int) async throws -> [Int : Int]
func fetchTodayProcedureList(date: String) async throws -> [ProcedureEntity]
}
🤖 Prompt for AI Agents
In `@Cherrish-iOS/Cherrish-iOS/Domain/Interface/CalendarInterface.swift` around
lines 10 - 13, The CalendarInterface methods are synchronous but the use cases
FetchProcedureCountOfMonth and FetchTodayProcedureList are declared async
throws; update the API to be consistent: either change CalendarInterface's
method signatures fetchProcedureCountOfMonth(year:month:) and
fetchTodayProcedureList(date:) to async throws if they may perform network/async
work (propagate errors) or change the use case declarations to synchronous
non-throwing variants to match the current implementation; pick the async throws
route if fetching can fail or is asynchronous, and ensure all conforming
implementations and callers (use case types) are updated accordingly.

Comment on lines +10 to +24
protocol FetchProcedureCountOfMonth {
func execute(year: Int, month: Int) async throws -> [Int: Int]
}

struct DefaultFetchProcedureCountOfMonth: FetchProcedureCountOfMonth {
private let repository: CalendarInterface

init(repository: CalendarInterface) {
self.repository = repository
}

func execute(year: Int, month: Int) async throws -> [Int : Int] {
return repository.fetchProcedureCountOfMonth(year: year, month: month)
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

async throws 시그니처가 동기 레포지토리 호출과 불일치합니다.

FetchTodayProcedureList와 동일한 문제입니다. execute(year:month:)async throws로 선언되어 있지만, 실제 레포지토리 메서드는 동기/비에러입니다.

일관성을 위해 CalendarInterface 전체를 async throws로 변경하거나, 두 유스케이스 모두 동기로 맞춰주세요.

🤖 Prompt for AI Agents
In `@Cherrish-iOS/Cherrish-iOS/Domain/UseCase/FetchProcedureCountOfMonth.swift`
around lines 10 - 24, The execute signature on FetchProcedureCountOfMonth (and
DefaultFetchProcedureCountOfMonth.execute) is declared as async throws but calls
a synchronous, non-throwing repository method
repository.fetchProcedureCountOfMonth(year:month:); fix the mismatch by choosing
one approach: either (A) update CalendarInterface.fetchProcedureCountOfMonth to
be async throws (and propagate that change to all implementations and other use
cases like FetchTodayProcedureList), or (B) change
FetchProcedureCountOfMonth.execute and DefaultFetchProcedureCountOfMonth.execute
to be synchronous non-throwing (remove async throws) to match the current
repository; locate the protocol FetchProcedureCountOfMonth, struct
DefaultFetchProcedureCountOfMonth, and the CalendarInterface method to apply the
chosen change consistently.

Comment on lines +65 to +85
Image(.chevronLeft)
.frame(width: 40, height: 40)
.scaledToFit()
.onTapGesture {
viewModel.currentMonth -= 1
viewModel.selectedDate = viewModel.firstDateOfCurrentMonth()
}

Spacer()

TypographyText("\(viewModel.getYearAndMonthString())", style: .title2_m_16, color: .gray1000)

Spacer()

Image(.chevronRight)
.frame(width: 40, height: 40)
.scaledToFit()
.onTapGesture {
viewModel.currentMonth += 1
viewModel.selectedDate = viewModel.firstDateOfCurrentMonth()
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

월 이동 후 데이터 재조회가 없어 이전 달 데이터가 남습니다.

Line 68-85에서 currentMonth/selectedDate만 변경하고 fetchProcedureCountsOfMonth/fetchTodayProcedureList를 호출하지 않아 목록과 카운트가 이전 달 값으로 남습니다. 월 변경 시 재조회가 필요합니다.

🐛 제안 수정
                 Image(.chevronLeft)
                     .frame(width: 40, height: 40)
                     .scaledToFit()
                     .onTapGesture {
                         viewModel.currentMonth -= 1
                         viewModel.selectedDate = viewModel.firstDateOfCurrentMonth()
+                        Task {
+                            do {
+                                try await viewModel.fetchProcedureCountsOfMonth()
+                                try await viewModel.fetchTodayProcedureList()
+                            } catch {
+                                CherrishLogger.error(error)
+                            }
+                        }
                     }
@@
                 Image(.chevronRight)
                     .frame(width: 40, height: 40)
                     .scaledToFit()
                     .onTapGesture {
                         viewModel.currentMonth += 1
                         viewModel.selectedDate = viewModel.firstDateOfCurrentMonth()
+                        Task {
+                            do {
+                                try await viewModel.fetchProcedureCountsOfMonth()
+                                try await viewModel.fetchTodayProcedureList()
+                            } catch {
+                                CherrishLogger.error(error)
+                            }
+                        }
                     }
🤖 Prompt for AI Agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarView.swift`
around lines 65 - 85, When changing months in the calendar (the
Image(.chevronLeft) and Image(.chevronRight) onTapGesture handlers), you're only
updating viewModel.currentMonth and viewModel.selectedDate via
viewModel.firstDateOfCurrentMonth(), but not reloading data; call the viewModel
methods that refresh the displayed data—specifically invoke
fetchProcedureCountsOfMonth() and fetchTodayProcedureList() (or the existing
viewModel refresh method) immediately after updating currentMonth and
selectedDate so the counts and list reflect the newly selected month.

Comment on lines +10 to +14
struct DateValue: Identifiable, Hashable {
var id: String = UUID().uuidString
var day: Int
var date: Date
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

DateValue.id가 매번 랜덤이라 SwiftUI diff가 불안정합니다.

Line 10-14에서 UUID()getDatesArray() 호출마다 새 id를 생성해 셀 재생성이 과도해질 수 있습니다. 날짜 기반의 안정적인 id를 권장합니다.

♻️ 제안 수정
 struct DateValue: Identifiable, Hashable {
-    var id: String = UUID().uuidString
     var day: Int
     var date: Date
+    var id: String { "\(date.timeIntervalSince1970)-\(day)" }
 }
📝 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
struct DateValue: Identifiable, Hashable {
var id: String = UUID().uuidString
var day: Int
var date: Date
}
struct DateValue: Identifiable, Hashable {
var day: Int
var date: Date
var id: String { "\(date.timeIntervalSince1970)-\(day)" }
}
🤖 Prompt for AI Agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarViewModel.swift`
around lines 10 - 14, DateValue's id is unstable because it uses UUID() causing
cell churn each time getDatesArray() recreates values; change id to be
deterministically derived from the date (for example make id a computed property
based on date like date.timeIntervalSince1970 or an ISO8601/yyyymmdd formatted
string) so Identifiable stability is preserved; update the DateValue struct (and
any places constructing it) to remove the random UUID and use the date-derived
id (keep day and date fields unchanged) so SwiftUI diffs are stable.

Comment on lines +10 to +19
struct GradientBox: View {
let isTop: Bool

var body: some View {
Rectangle()
.fill(LinearGradient(gradient: isTop ? Gradient(colors: [.gray0, .gray00]) : Gradient(colors: [.gray00, .gray0]),
startPoint: .top,
endPoint: .bottom)
)
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

LGTM!

스크롤 그라데이션 효과를 위한 깔끔한 구현입니다. 그라데이션 방향 로직이 올바르게 작동합니다.

개발 편의를 위해 #Preview를 추가하는 것도 고려해 보세요 (선택 사항):

✨ Preview 추가 제안
     }
 }
+
+#Preview {
+    VStack {
+        GradientBox(isTop: true)
+            .frame(height: 40)
+        Spacer()
+        GradientBox(isTop: false)
+            .frame(height: 40)
+    }
+}
🤖 Prompt for AI Agents
In `@Cherrish-iOS/Cherrish-iOS/Presentation/Global/Components/GradientBox.swift`
around lines 10 - 19, Add a SwiftUI preview for the GradientBox view so
reviewers and designers can see both states; create a PreviewProvider
referencing GradientBox and supply examples for isTop = true and isTop = false
(e.g., in a VStack or ForEach), include a fixed frame or
.previewLayout(.sizeThatFits) and any background contrast so the gradient is
visible, and ensure the preview uses the same struct name GradientBox and its
isTop property.

Comment on lines +20 to +27
func toDateString() -> String {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
formatter.locale = Locale(identifier: "ko_KR")
formatter.timeZone = TimeZone(secondsFromGMT: 0)

return formatter.string(from: self)
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

DateFormatter를 캐싱하여 성능 개선을 권장합니다.

DateFormatter는 생성 비용이 높은 객체입니다. 캘린더 UI에서 이 메서드가 각 셀마다 호출될 경우 성능 저하가 발생할 수 있습니다. static 프로퍼티로 캐싱하는 것을 권장합니다.

♻️ 제안된 수정
 extension Date {
+    private static let dateStringFormatter: DateFormatter = {
+        let formatter = DateFormatter()
+        formatter.dateFormat = "yyyy-MM-dd"
+        formatter.locale = Locale(identifier: "ko_KR")
+        formatter.timeZone = TimeZone(secondsFromGMT: 0)
+        return formatter
+    }()
+    
     func getAllDates() -> [Date] {
         // ...
     }
     
     func toDateString() -> String {
-        let formatter = DateFormatter()
-        formatter.dateFormat = "yyyy-MM-dd"
-        formatter.locale = Locale(identifier: "ko_KR")
-        formatter.timeZone = TimeZone(secondsFromGMT: 0)
-
-        return formatter.string(from: self)
+        return Date.dateStringFormatter.string(from: self)
     }
 }
🤖 Prompt for AI Agents
In `@Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/Date`+.swift around
lines 20 - 27, 현재 toDateString()에서 매 호출마다 DateFormatter를 생성해 성능 저하가 발생할 수 있으니
DateFormatter를 캐싱하도록 변경하세요: Date 확장에 static 프로퍼티(e.g. yyyyMMddFormatter)를 추가해
locale, timeZone, dateFormat을 한 번만 설정하고, toDateString()는 새 포매터를 만들지 말고 해당 static
프로퍼티(예: Date.yyyyMMddFormatter)를 사용해 문자열을 반환하도록 바꿉니다; 필요하면 멀티스레드 환경에서의 안전성을 고려해
호출 위치에서 동기화 또는 별도 포매터 사용 방안을 검토하세요.

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

🤖 Fix all issues with AI agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/SelectTreatment/SelectTreatmentView.swift`:
- Around line 41-47: The "다음" CherrishButton currently has an empty action
closure; replace the placeholder closure with the actual next-step logic—e.g.
call the SelectTreatmentView's viewModel method that advances navigation (invoke
an existing method like viewModel.onNextTapped() or add a new
viewModel.proceedToNext()/advanceSelection() and call it here) so tapping the
button moves to the next screen or validates selection; if this was
intentionally left blank, add a TODO comment inside the closure explaining why
and what method should be called.

Comment on lines +41 to +47
CherrishButton(
title: "다음",
type: .large,
state: $viewModel.buttonState,
leadingIcon: nil,
trailingIcon: nil,
action: { })
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

"다음" 버튼의 action이 비어있습니다.

버튼의 action 클로저가 { }로 비어있어 버튼을 탭해도 아무 동작도 수행하지 않습니다. 시술 선택 후 다음 화면으로 네비게이션하는 로직이 필요해 보입니다.

의도적으로 placeholder로 남겨둔 것이라면 TODO 주석을 추가하거나, 실제 네비게이션 로직을 구현해 주세요.

🔧 예시 수정안
 CherrishButton(
     title: "다음",
     type: .large,
     state: $viewModel.buttonState,
     leadingIcon: nil,
     trailingIcon: nil,
-    action: { })
+    action: {
+        // TODO: 다음 화면으로 네비게이션 구현 필요
+        // calendarCoordinator.push(...)
+    })
📝 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
CherrishButton(
title: "다음",
type: .large,
state: $viewModel.buttonState,
leadingIcon: nil,
trailingIcon: nil,
action: { })
CherrishButton(
title: "다음",
type: .large,
state: $viewModel.buttonState,
leadingIcon: nil,
trailingIcon: nil,
action: {
// TODO: 다음 화면으로 네비게이션 구현 필요
// calendarCoordinator.push(...)
})
🤖 Prompt for AI Agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/SelectTreatment/SelectTreatmentView.swift`
around lines 41 - 47, The "다음" CherrishButton currently has an empty action
closure; replace the placeholder closure with the actual next-step logic—e.g.
call the SelectTreatmentView's viewModel method that advances navigation (invoke
an existing method like viewModel.onNextTapped() or add a new
viewModel.proceedToNext()/advanceSelection() and call it here) so tapping the
button moves to the next screen or validates selection; if this was
intentionally left blank, add a TODO comment inside the closure explaining why
and what method should be called.

@y-eonee y-eonee force-pushed the style/#39-캘린더 branch from b0cd33b to 4d0617c Compare January 18, 2026 14:57
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: 5

🤖 Fix all issues with AI agents
In `@Cherrish-iOS/Cherrish-iOS/Data/DataDependencyAssembler.swift`:
- Around line 18-19: DataDependencyAssembler currently always registers
MockCalendarRepository for CalendarInterface which can leak mocks into release
builds; change the registration in DIContainer.shared.register(type:
CalendarInterface.self) to conditionally register MockCalendarRepository only
for debug/testing builds and register the real implementation (e.g.,
RealCalendarRepository or ProductionCalendarRepository) for release by using a
build configuration flag or compiler directive (e.g., `#if` DEBUG) around the
registration so MockCalendarRepository is not injected in production.

In `@Cherrish-iOS/Cherrish-iOS/Data/Repository/CalendarRepository.swift`:
- Around line 31-118: fetchTodayProcedureList currently ignores its date
parameter and always returns the same hard-coded ProcedureEntity array; change
it to use the date argument to decide which procedures apply (either by
filtering each ProcedureEntity's sensitiveDays/cautionDays/recoveryDays for the
given date or by replacing the hard-coded list with a date→[ProcedureEntity]
mapping and returning mapping[date] ?? []). Locate fetchTodayProcedureList and
update its logic to consider the input date (and use ProcedureEntity properties
sensitiveDays/cautionDays/recoveryDays) so callers get a date-specific list;
ensure the function still returns [ProcedureEntity] when no entry exists (e.g.,
return empty array).
- Around line 21-28: The hard-coded dictionary in
fetchProcedureCountOfMonth(year:month:) ignores the month parameter and can
return counts for invalid days (e.g., Feb/30). Replace the static map with logic
that computes the number of days for the given year/month (using Calendar and
DateComponents), then build and return a dictionary containing only valid day
keys (1...daysInMonth) populated either from real data or generated/dummy counts
as needed; ensure any seed data is filtered by day <= daysInMonth before
returning.

In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarCellView.swift`:
- Around line 32-38: The Circle can render as an oval because it uses different
width and height (40.adjustedW vs 40.adjustedH); change the layout to use a
single square size (e.g., compute a single size from 40.adjustedW and
40.adjustedH such as min or max) and pass that same value to both frame width
and height where Circle() is configured (the Circle overlay/stroke uses
downtimeState.backgroundColor and downtimeState.strokeColor); alternatively
enforce a 1:1 aspect ratio for the Circle to guarantee a true circle in
CalendarCellView's Circle() frame.

In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/ChallengeProgressView.swift`:
- Around line 158-166: The "오늘 미션 종료하기" CherrishButton currently has an empty
action closure so tapping does nothing; implement the action to invoke the
intended mission completion logic (e.g., call the view model or coordinator
method that handles finishing today's mission). Locate the CherrishButton
instantiation in ChallengeProgressView and replace the empty closure with a call
to the appropriate handler (for example, viewModel.finishMission(),
viewModel.completeTodayMission(), or coordinator.finishMission()), update any
related UI state (buttonState) and handle success/error (show toast/alert or
update state) so the button performs the mission-end workflow.
♻️ Duplicate comments (20)
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/String+.swift (1)

11-23: timeZone 명시가 여전히 누락되어 있습니다.

DateFormatter 캐싱은 잘 적용되었습니다. 하지만 이전 리뷰에서 언급된 timeZone 명시가 아직 반영되지 않았습니다. 서버 응답 날짜가 UTC나 KST 기준일 경우, 디바이스 로컬 타임존으로 파싱하면 자정 근처에서 날짜가 하루 어긋날 수 있습니다.

💡 제안 수정안
 private static let inputFormatter: DateFormatter = {
     let dateFormatter = DateFormatter()
     dateFormatter.dateFormat = "yyyy-MM-dd"
     dateFormatter.locale = Locale(identifier: "en_US_POSIX")
+    dateFormatter.timeZone = TimeZone(identifier: "Asia/Seoul") // 서버 기준에 맞게 설정
     return dateFormatter
 }()

 private static let outputFormatter: DateFormatter = {
     let dateFormatter = DateFormatter()
     dateFormatter.dateFormat = "M월 d일 EEEE"
     dateFormatter.locale = Locale(identifier: "ko_KR")
+    dateFormatter.timeZone = TimeZone(identifier: "Asia/Seoul")
     return dateFormatter
 }()
Cherrish-iOS/Cherrish-iOS/Assets.xcassets/Color/gray_0_0.colorset/Contents.json (1)

7-7: 투명도 설정 오류: 색상이 완전히 투명합니다.

alpha"0.000"으로 설정되어 있어 이 색상은 화면에 표시되지 않습니다. 다른 회색 자산들(gray_0, gray_100 등)은 모두 alpha : "1.000"(불투명)을 사용합니다.

또한 gray_0_0이라는 명명은 프로젝트의 회색 색상 체계(gray_0, gray_100, gray_200 등)와 일치하지 않습니다.

🔧 수정 제안

만약 이 자산이 실수로 생성된 것이라면 삭제하고, 의도된 자산이라면 다음과 같이 수정해야 합니다:

  1. 파일명을 gray_0 또는 적절한 명명 규칙에 맞게 변경
  2. alpha 값을 "1.000"으로 수정
         "components" : {
-          "alpha" : "0.000",
+          "alpha" : "1.000",
           "blue" : "0xFF",
           "green" : "0xFF",
           "red" : "0xFF"

Also applies to: 25-25

Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/Date+.swift (2)

11-17: 강제 언래핑과 Date() 폴백 제거가 필요합니다.

Calendar.date(from:)/range(of:)가 nil일 수 있어 크래시 또는 잘못된 날짜가 섞일 수 있습니다.

🐛 제안 수정
 func getAllDates() -> [Date] {
     let calendar = Calendar.current
-    let startDate = calendar.date(from: Calendar.current.dateComponents([.year, .month], from: self))!
-    let range = calendar.range(of: .day, in: .month, for: startDate)!
-    return range.compactMap { day -> Date in
-        calendar.date(byAdding: .day, value: day - 1, to: startDate) ?? Date()
-    }
+    guard let startDate = calendar.date(from: calendar.dateComponents([.year, .month], from: self)),
+          let range = calendar.range(of: .day, in: .month, for: startDate)
+    else { return [] }
+    return range.compactMap { day in
+        calendar.date(byAdding: .day, value: day - 1, to: startDate)
+    }
 }

20-26: DateFormatter 캐싱으로 성능 개선을 권장합니다.

셀 단위 호출 시 생성 비용이 누적될 수 있습니다.

♻️ 제안 수정
 extension Date {
+    private static let dateStringFormatter: DateFormatter = {
+        let formatter = DateFormatter()
+        formatter.dateFormat = "yyyy-MM-dd"
+        formatter.locale = Locale(identifier: "ko_KR")
+        formatter.timeZone = TimeZone(secondsFromGMT: 0)
+        return formatter
+    }()
+
     func toDateString() -> String {
-        let formatter = DateFormatter()
-        formatter.dateFormat = "yyyy-MM-dd"
-        formatter.locale = Locale(identifier: "ko_KR")
-        formatter.timeZone = TimeZone(secondsFromGMT: 0)
-
-        return formatter.string(from: self)
+        return Date.dateStringFormatter.string(from: self)
     }
 }
Cherrish-iOS/Cherrish-iOS/Domain/Interface/CalendarInterface.swift (1)

10-12: 인터페이스와 유스케이스의 async/throws 정합성을 맞춰주세요.

동기 시그니처와 async/throws 유스케이스 간 불일치가 있으면 호출부 정합성이 깨집니다.

🔧 예시 수정(비동기 필요 시)
 protocol CalendarInterface {
-    func fetchProcedureCountOfMonth(year: Int, month: Int) -> [Int : Int]
-    func fetchTodayProcedureList(date: String) -> [ProcedureEntity]
+    func fetchProcedureCountOfMonth(year: Int, month: Int) async throws -> [Int : Int]
+    func fetchTodayProcedureList(date: String) async throws -> [ProcedureEntity]
 }
Cherrish-iOS/Cherrish-iOS/Data/Model/CalendarDailyResponseDTO.swift (1)

27-30: toEntity()가 비어 있어 변환이 누락됩니다.

사용된다면 실제 매핑 구현이 필요하고, 미사용이면 즉시 실패하도록 처리하는 편이 안전합니다.

✅ 대안(미사용 시 fail-fast)
 extension CalendarDailyResponseDTO {
-    func toEntity() {
-        
-    }
+    func toEntity() -> Never {
+        fatalError("CalendarDailyResponseDTO.toEntity() is not implemented")
+    }
 }
Cherrish-iOS/Cherrish-iOS/Domain/UseCase/FetchProcedureCountOfMonth.swift (1)

10-23: async/throws 시그니처가 동기 레포지토리와 불일치합니다.

레포지토리 호출이 동기/비에러인데 use case가 async/throws라 호출부에 불필요한 await/try가 전파됩니다. 하나의 방향으로 통일해 주세요.

♻️ (동기화 선택 시) 제안 수정
 protocol FetchProcedureCountOfMonth {
-    func execute(year: Int, month: Int) async throws -> [Int: Int]
+    func execute(year: Int, month: Int) -> [Int: Int]
 }

 struct DefaultFetchProcedureCountOfMonth: FetchProcedureCountOfMonth {
@@
-    func execute(year: Int, month: Int) async throws -> [Int : Int] {
+    func execute(year: Int, month: Int) -> [Int : Int] {
         return repository.fetchProcedureCountOfMonth(year: year, month: month)
     }
 }
Cherrish-iOS/Cherrish-iOS/Domain/UseCase/FetchTodayProcedureList.swift (1)

10-22: async/throws 시그니처가 동기 레포지토리와 불일치합니다.

레포지토리 메서드가 동기/비에러인데 use case가 async/throws로 선언되어 호출부를 복잡하게 만듭니다. 동일한 정책으로 맞춰주세요.

♻️ (동기화 선택 시) 제안 수정
 protocol FetchTodayProcedureList {
-    func execute(date: String) async throws -> [ProcedureEntity]
+    func execute(date: String) -> [ProcedureEntity]
 }

 struct DefaultFetchTodayProcedure: FetchTodayProcedureList {
@@
-    func execute(date: String) async throws -> [ProcedureEntity] {
+    func execute(date: String) -> [ProcedureEntity] {
         repository.fetchTodayProcedureList(date: date)
     }
 }
Cherrish-iOS/Cherrish-iOS/Domain/DomainDependencyAssembler.swift (1)

20-26: DI resolve 실패/캡처 시점 문제로 디버깅 및 재조립 안정성이 떨어집니다.

assemble 시점에 calendarRepository를 캡처하면 DI 재조립 시 stale 인스턴스가 남을 수 있고, resolve 실패 시 조용히 반환되어 원인 파악이 어렵습니다. 클로저 내부에서 lazy resolve + 로깅을 권장합니다.

♻️ 제안 수정
-        guard let calendarRepository = DIContainer.shared.resolve(type: CalendarInterface.self) else {
-            return
-        }
-        
         DIContainer.shared.register(type: FetchProcedureCountOfMonth.self) {
-            return DefaultFetchProcedureCountOfMonth(repository: calendarRepository)
+            guard let calendarRepository = DIContainer.shared.resolve(type: CalendarInterface.self) else {
+                assertionFailure("CalendarInterface DI resolve failed")
+                fatalError("CalendarInterface DI resolve failed")
+            }
+            return DefaultFetchProcedureCountOfMonth(repository: calendarRepository)
         }
@@
         DIContainer.shared.register(type: FetchTodayProcedureList.self) {
-            return DefaultFetchTodayProcedure(repository: calendarRepository)
+            guard let calendarRepository = DIContainer.shared.resolve(type: CalendarInterface.self) else {
+                assertionFailure("CalendarInterface DI resolve failed")
+                fatalError("CalendarInterface DI resolve failed")
+            }
+            return DefaultFetchTodayProcedure(repository: calendarRepository)
         }

Also applies to: 36-38

Cherrish-iOS/Cherrish-iOS/Presentation/Global/Components/CherrishButton.swift (1)

27-48: 아이콘 색상 동기화 누락

텍스트 색상과 동일하게 아이콘도 상태별 색상 적용이 필요합니다.

♻️ 수정 제안
                 if let leadingIcon {
                     leadingIcon
+                        .renderingMode(.template)
+                        .foregroundStyle(type.textColor(for: state))
                         .frame(width: 24, height: 24)
                 }
                 
                 Text(title)
                     .typography(.title2_sb_16)
                     .foregroundStyle(type.textColor(for: state))
                 
                 if let trailingIcon {
                     trailingIcon
+                        .renderingMode(.template)
+                        .foregroundStyle(type.textColor(for: state))
                         .frame(width: 24, height: 24)
                 }
Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/SelectTreatment/SelectTreatmentView.swift (1)

41-47: "다음" 버튼 action이 비어있음

탭해도 동작이 없어 흐름이 막힙니다.

🔧 수정 제안
             CherrishButton(
                 title: "다음",
                 type: .large,
                 state: $viewModel.buttonState,
                 leadingIcon: nil,
                 trailingIcon: nil,
-                action: { })
+                action: {
+                    // TODO: 다음 화면 네비게이션 로직 연결
+                    // viewModel.onNextTapped() 또는 coordinator.push(...)
+                })
Cherrish-iOS/Cherrish-iOS/Presentation/ViewFactory.swift (1)

47-49: DI 실패 시 에러 메시지 추가 필요

원인 파악을 위해 최소한의 메시지가 필요합니다.

💡 수정 제안
         guard let viewModel = DIContainer.shared.resolve(type: CalendarViewModel.self) else {
-            fatalError()
+            fatalError("CalendarViewModel DI resolve failed in ViewFactory.makeCalendarView()")
         }
Cherrish-iOS/Cherrish-iOS/Presentation/PresentationDependencyAssembler.swift (1)

24-36: 등록 클로저 내부에서 lazy resolve로 변경 필요

이전 리뷰에서 지적된 것처럼, assemble() 시점에 use case 인스턴스를 캡처하면 이후 DI 스코프 변경(로그아웃/재조립 등) 시 stale 의존성이 남을 수 있습니다. 등록 클로저 내부에서 resolve하도록 변경해야 합니다.

♻️ 권장 수정안
-        guard let fetchProcedureCountOfMonthUseCase = DIContainer.shared.resolve(type: FetchProcedureCountOfMonth.self),
-            let fetchTodayProcedureListUseCase = DIContainer.shared.resolve(type: FetchTodayProcedureList.self)
-        else {
-            CherrishLogger.error(CherrishError.DIFailedError)
-            return
-        }
-        
         DIContainer.shared.register(type: CalendarViewModel.self) {
+            guard let fetchProcedureCountOfMonthUseCase = DIContainer.shared.resolve(type: FetchProcedureCountOfMonth.self),
+                  let fetchTodayProcedureListUseCase = DIContainer.shared.resolve(type: FetchTodayProcedureList.self)
+            else {
+                CherrishLogger.error(CherrishError.DIFailedError)
+                fatalError("CalendarViewModel DI resolve failed")
+            }
             return CalendarViewModel(
                 fetchProcedureCountOfMonthUseCase: fetchProcedureCountOfMonthUseCase,
                 fetchTodayProcedureListUseCase: fetchTodayProcedureListUseCase
             )
         }
Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarView.swift (4)

27-30: 중복된 nil 초기화 제거 필요

SwiftLint에서 Optional 변수의 = nil 초기화가 중복이라고 경고합니다. Swift에서 Optional은 기본적으로 nil로 초기화됩니다.

♻️ 제안하는 수정
-    `@State` private var initialTopGlobalY: CGFloat? = nil
+    `@State` private var initialTopGlobalY: CGFloat?
     `@State` private var bottomOffsetY: CGFloat = .zero
     `@State` private var calendarMode: CalendarMode = .none
-    `@State` private var selectedProcedureID: Int? = nil
+    `@State` private var selectedProcedureID: Int?

68-85: 월 이동 시 데이터 재조회 누락

이전/다음 월로 이동할 때 currentMonthselectedDate만 업데이트하고 fetchProcedureCountsOfMonth()fetchTodayProcedureList()를 호출하지 않아 이전 달 데이터가 그대로 표시됩니다.

🐛 제안하는 수정
                 Image(.chevronLeft)
                     .frame(width: 40, height: 40)
                     .scaledToFit()
                     .onTapGesture {
                         viewModel.currentMonth -= 1
                         viewModel.selectedDate = viewModel.firstDateOfCurrentMonth()
+                        Task {
+                            do {
+                                try await viewModel.fetchProcedureCountsOfMonth()
+                                try await viewModel.fetchTodayProcedureList()
+                            } catch {
+                                CherrishLogger.error(error)
+                            }
+                        }
                     }

170-174: 다른 시술 항목 선택 시 2탭 필요 문제

현재 calendarMode.toggle()을 무조건 호출하므로, 선택된 상태에서 다른 항목을 탭하면 모드가 .none으로 전환됩니다. 다른 항목을 바로 선택하려면 2번 탭해야 합니다.

🐛 제안하는 수정
                         .onTapGesture {
-                            calendarMode.toggle()
-                            selectedProcedureID = procedure.procedureId
-                            viewModel.fetchDowntimeByDay(procedureId: procedure.procedureId)
+                            let isSame = selectedProcedureID == procedure.procedureId
+                            calendarMode = isSame ? .none : .selectedProcedure
+                            selectedProcedureID = isSame ? nil : procedure.procedureId
+                            if !isSame {
+                                viewModel.fetchDowntimeByDay(procedureId: procedure.procedureId)
+                            }
                         }

286-288: iOS 17+ onChange API 업데이트 필요

단일 파라미터 형식의 onChange(of:)는 iOS 17부터 deprecated되었습니다. 새로운 API로 마이그레이션이 필요합니다.

.onChange(of: proxy.frame(in: .global).minY) { _, newValue in
    topGlobalY = newValue
}
Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarViewModel.swift (3)

10-14: DateValue.id의 불안정성으로 SwiftUI 셀 재생성 과다

UUID().uuidStringgetDatesArray() 호출마다 새 id를 생성해 SwiftUI가 모든 셀을 불필요하게 재생성합니다. 날짜 기반의 안정적인 id를 사용하세요.

♻️ 제안하는 수정
 struct DateValue: Identifiable, Hashable {
-    var id: String = UUID().uuidString
     var day: Int
     var date: Date
+    
+    var id: String {
+        "\(Int(date.timeIntervalSince1970))-\(day)"
+    }
 }

93-99: @MainActor 누락으로 잠재적 스레드 안전성 문제

fetchDowntimeByDay@Published 속성인 downtimeByDay를 수정하지만 @MainActor가 없습니다. 현재는 UI에서 동기적으로 호출되어 문제없지만, 향후 백그라운드에서 호출될 경우 문제가 발생할 수 있습니다.

♻️ 제안하는 수정
+    `@MainActor`
     func fetchDowntimeByDay(procedureId: Int) {
         guard let procedure = procedureList.first(where: { $0.procedureId == procedureId }) else {
             downtimeByDay = [:]
             return
         }
         mapToDowntimeDays(procedure: procedure)
     }

117-133: 변수 섀도잉(Variable Shadowing)으로 가독성 저하

Line 120에서 로컬 변수 currentMonth가 파라미터 currentMonth를 섀도잉합니다.

♻️ 제안하는 수정
     private func extractDate(currentMonth: Int) -> [DateValue] {
         let calendar = Calendar.current
         
-        let currentMonth = getCurrentMonth(addingMonth: currentMonth)
-        var days = currentMonth.getAllDates().compactMap { date -> DateValue in
+        let targetMonth = getCurrentMonth(addingMonth: currentMonth)
+        var days = targetMonth.getAllDates().compactMap { date -> DateValue in
             let day = calendar.component(.day, from: date)
             return DateValue(day: day, date: date)
         }

Comment on lines +18 to +19
DIContainer.shared.register(type: CalendarInterface.self) {
return MockCalendarRepository()
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Mock 레포지토리가 항상 주입되어 실제 데이터 경로가 차단됩니다.

현재 구성은 릴리즈에서도 목 데이터가 노출될 수 있습니다. 빌드 설정이나 플래그로 분기해 주세요.

🐛 제안 수정
 DIContainer.shared.register(type: CalendarInterface.self) {
-    return MockCalendarRepository()
+    `#if` DEBUG
+    return MockCalendarRepository()
+    `#else`
+    return DefaultCalendarRepository()
+    `#endif`
 }
🤖 Prompt for AI Agents
In `@Cherrish-iOS/Cherrish-iOS/Data/DataDependencyAssembler.swift` around lines 18
- 19, DataDependencyAssembler currently always registers MockCalendarRepository
for CalendarInterface which can leak mocks into release builds; change the
registration in DIContainer.shared.register(type: CalendarInterface.self) to
conditionally register MockCalendarRepository only for debug/testing builds and
register the real implementation (e.g., RealCalendarRepository or
ProductionCalendarRepository) for release by using a build configuration flag or
compiler directive (e.g., `#if` DEBUG) around the registration so
MockCalendarRepository is not injected in production.

Comment on lines +21 to +28
func fetchProcedureCountOfMonth(year: Int, month: Int) -> [Int : Int] {
return [
1: 2,
7: 5,
15: 1,
23: 3,
31: 6
]
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

month 파라미터 무시로 잘못된 날짜 카운트가 표시될 수 있습니다.

항상 31일을 포함하는 고정 딕셔너리는 30일/2월 같은 달에서 존재하지 않는 날짜에 카운트를 표시할 수 있습니다. year/month에 맞춰 필터링하거나 더미를 동적으로 생성해 주세요.

🐛 제안 수정
 func fetchProcedureCountOfMonth(year: Int, month: Int) -> [Int : Int] {
-    return [
+    let base: [Int: Int] = [
     ]
+    let calendar = Calendar.current
+    let date = calendar.date(from: DateComponents(year: year, month: month)) ?? Date()
+    let range = calendar.range(of: .day, in: .month, for: date) ?? 1..<32
+    return base.filter { range.contains($0.key) }
 }
🤖 Prompt for AI Agents
In `@Cherrish-iOS/Cherrish-iOS/Data/Repository/CalendarRepository.swift` around
lines 21 - 28, The hard-coded dictionary in
fetchProcedureCountOfMonth(year:month:) ignores the month parameter and can
return counts for invalid days (e.g., Feb/30). Replace the static map with logic
that computes the number of days for the given year/month (using Calendar and
DateComponents), then build and return a dictionary containing only valid day
keys (1...daysInMonth) populated either from real data or generated/dummy counts
as needed; ensure any seed data is filtered by day <= daysInMonth before
returning.

Comment on lines +31 to +118
func fetchTodayProcedureList(date: String) -> [ProcedureEntity] {
return [
ProcedureEntity(
procedureId: 1,
name: "레이저 토닝",
downtimeDays: 7,
sensitiveDays: [
"2026-01-15",
"2026-01-16",
"2026-01-17"
],
cautionDays: [
"2026-01-18",
"2026-01-19"
],
recoveryDays: [
"2026-01-20",
"2026-01-21"
]
),
ProcedureEntity(
procedureId: 2,
name: "보톡스",
downtimeDays: 3,
sensitiveDays: [
"2026-01-15"
],
cautionDays: [
"2026-01-16"
],
recoveryDays: [
"2026-01-17"
]
),
ProcedureEntity(
procedureId: 3,
name: "필러",
downtimeDays: 5,
sensitiveDays: [
"2026-01-15",
"2026-01-16"
],
cautionDays: [
"2026-01-17",
"2026-01-18"
],
recoveryDays: [
"2026-01-19"
]
),
ProcedureEntity(
procedureId: 4,
name: "IPL 레이저",
downtimeDays: 2,
sensitiveDays: [
"2026-01-15"
],
cautionDays: [
"2026-01-16"
],
recoveryDays: []
),
ProcedureEntity(
procedureId: 5,
name: "윤곽 주사",
downtimeDays: 4,
sensitiveDays: [
"2026-01-15",
"2026-01-16"
],
cautionDays: [
"2026-01-17"
],
recoveryDays: [
"2026-01-18"
]
),
ProcedureEntity(
procedureId: 6,
name: "피부 스케일링",
downtimeDays: 0,
sensitiveDays: [
"2026-01-15"
],
cautionDays: [],
recoveryDays: []
)
]
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

date 파라미터를 무시해 어떤 날짜에서도 동일 목록이 반환됩니다.

현재 구현은 선택 날짜와 무관하게 항상 동일한 시술 목록을 반환합니다. 캘린더 UI 테스트 시에도 날짜별 상태를 구분할 수 없으므로, 날짜에 따라 필터링하거나 date→list 매핑을 쓰는 방식이 안전합니다.

🐛 제안 수정
 func fetchTodayProcedureList(date: String) -> [ProcedureEntity] {
-    return [
+    let all: [ProcedureEntity] = [
         ProcedureEntity(
             procedureId: 1,
             name: "레이저 토닝",
             downtimeDays: 7,
             sensitiveDays: [
                 "2026-01-15",
                 "2026-01-16",
                 "2026-01-17"
             ],
             cautionDays: [
                 "2026-01-18",
                 "2026-01-19"
             ],
             recoveryDays: [
                 "2026-01-20",
                 "2026-01-21"
             ]
         ),
         // ... 이하 동일
         ProcedureEntity(
             procedureId: 6,
             name: "피부 스케일링",
             downtimeDays: 0,
             sensitiveDays: [
                 "2026-01-15"
             ],
             cautionDays: [],
             recoveryDays: []
         )
     ]
+    return all.filter {
+        $0.sensitiveDays.contains(date)
+        || $0.cautionDays.contains(date)
+        || $0.recoveryDays.contains(date)
+    }
 }
🤖 Prompt for AI Agents
In `@Cherrish-iOS/Cherrish-iOS/Data/Repository/CalendarRepository.swift` around
lines 31 - 118, fetchTodayProcedureList currently ignores its date parameter and
always returns the same hard-coded ProcedureEntity array; change it to use the
date argument to decide which procedures apply (either by filtering each
ProcedureEntity's sensitiveDays/cautionDays/recoveryDays for the given date or
by replacing the hard-coded list with a date→[ProcedureEntity] mapping and
returning mapping[date] ?? []). Locate fetchTodayProcedureList and update its
logic to consider the input date (and use ProcedureEntity properties
sensitiveDays/cautionDays/recoveryDays) so callers get a date-specific list;
ensure the function still returns [ProcedureEntity] when no entry exists (e.g.,
return empty array).

Comment on lines +32 to +38
Circle()
.fill(downtimeState.backgroundColor)
.overlay(
Circle()
.stroke(downtimeState.strokeColor, lineWidth: 1)
)
.frame(width: 40.adjustedW, height: 40.adjustedH)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Circle이 타원형으로 렌더링될 수 있음

adjustedWadjustedH가 서로 다른 값을 반환하면 Circle이 타원형으로 렌더링될 수 있습니다. 원형을 유지하려면 width와 height에 동일한 값을 사용하는 것이 안전합니다.

♻️ 권장 수정안
                     .frame(width: 40.adjustedW, height: 40.adjustedH)
+                    // 또는 원형 유지를 위해:
+                    // .frame(width: 40.adjustedW, height: 40.adjustedW)
🤖 Prompt for AI Agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarCellView.swift`
around lines 32 - 38, The Circle can render as an oval because it uses different
width and height (40.adjustedW vs 40.adjustedH); change the layout to use a
single square size (e.g., compute a single size from 40.adjustedW and
40.adjustedH such as min or max) and pass that same value to both frame width
and height where Circle() is configured (the Circle overlay/stroke uses
downtimeState.backgroundColor and downtimeState.strokeColor); alternatively
enforce a 1:1 aspect ratio for the Circle to guarantee a true circle in
CalendarCellView's Circle() frame.

Comment on lines +158 to 166
CherrishButton(
title: "오늘 미션 종료하기",
type: .small,
state: .constant(buttonState),
leadingIcon: nil,
trailingIcon: nil
){

}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's locate and examine the file
fd "ChallengeProgressView.swift" --type f

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 158


🏁 Script executed:

# Once we find it, let's read the relevant section
cat -n Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/ChallengeProgressView.swift | sed -n '150,180p'

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 1161


🏁 Script executed:

# Let's also check the broader context around line 70 to understand buttonState
cat -n Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/ChallengeProgressView.swift | sed -n '60,80p'

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 929


🏁 Script executed:

# Search for other CherrishButton usages to see if empty closures are common pattern
rg "CherrishButton" Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/ChallengeProgressView.swift -A 6 -B 1

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 308


🏁 Script executed:

# Check if there are any TODOs or FIXME comments related to this button
rg "TODO|FIXME|미션 종료|오늘 미션" Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/ChallengeProgressView.swift

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 106


🏁 Script executed:

# Find CherrishButton definition
fd "CherrishButton" --type f | head -20

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 147


🏁 Script executed:

# Search for CherrishButton definition
rg "struct CherrishButton|func CherrishButton" --type swift -A 10

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 2566


🏁 Script executed:

# Check other uses of CherrishButton in the codebase to see if empty closures are pattern
rg "CherrishButton" --type swift -A 8 | grep -A 8 ")\{" | head -80

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 88


🏁 Script executed:

# Check all CherrishButton usages to see implementations of action closures
rg "CherrishButton\(" --type swift -A 10 | head -150

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 15946


🏁 Script executed:

# Count empty action closures vs non-empty ones in the codebase
rg "CherrishButton\(" --type swift -A 10 | grep -E "^\s*\){$" | wc -l

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 71


🏁 Script executed:

# Look at the actual CherrishButton.swift to understand full context
cat -n Cherrish-iOS/Cherrish-iOS/Presentation/Global/Components/CherrishButton.swift | head -50

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 1521


버튼 액션 미구현 - 버튼이 작동하지 않음

"오늘 미션 종료하기" 버튼의 액션 클로저가 비어있어 버튼을 탭해도 아무 동작도 하지 않습니다. CherrishButton은 action 클로저를 필수 파라미터로 요구하며 이를 Button(action:)에 전달하므로, 빈 클로저는 버튼이 완전히 비활성화된 상태를 의미합니다. 버튼의 의도된 동작(미션 종료 처리 등)을 구현해야 합니다.

🤖 Prompt for AI Agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/ChallengeProgressView.swift`
around lines 158 - 166, The "오늘 미션 종료하기" CherrishButton currently has an empty
action closure so tapping does nothing; implement the action to invoke the
intended mission completion logic (e.g., call the view model or coordinator
method that handles finishing today's mission). Locate the CherrishButton
instantiation in ChallengeProgressView and replace the empty closure with a call
to the appropriate handler (for example, viewModel.finishMission(),
viewModel.completeTodayMission(), or coordinator.finishMission()), update any
related UI state (buttonState) and handle success/error (show toast/alert or
update state) so the button performs the mission-end workflow.

@y-eonee y-eonee merged commit d837006 into develop Jan 18, 2026
1 check passed
@y-eonee y-eonee deleted the style/#39-캘린더 branch January 18, 2026 15:13
Kimgyuilli pushed a commit that referenced this pull request Jan 21, 2026
y-eonee added a commit that referenced this pull request Jan 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Style] 캘린더

5 participants