Skip to content

Comments

Feat/#99 캘린더 API 연결#118

Merged
y-eonee merged 20 commits intodevelopfrom
feat/#99-캘린더-api
Jan 20, 2026

Hidden character warning

The head ref may contain hidden characters: "feat/#99-\uce98\ub9b0\ub354-api"
Merged

Feat/#99 캘린더 API 연결#118
y-eonee merged 20 commits intodevelopfrom
feat/#99-캘린더-api

Conversation

@y-eonee
Copy link
Contributor

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

🔗 연결된 이슈

📄 작업 내용

  • 캘린더 API 연결했습니다
  • 캘린더 뷰 수정사항 108번 브랜치에서 수정했었는데 108번 베이스로 이 브랜치를 파가지고 걔는 이슈 닫고 얘만 머지하겠습니당
구현 내용 IPhone 13
GIF

@y-eonee y-eonee requested a review from a team January 20, 2026 08:07
@y-eonee y-eonee self-assigned this Jan 20, 2026
@y-eonee y-eonee requested review from soseoyo12, sum130 and wotjs020708 and removed request for a team January 20, 2026 08:08
@y-eonee y-eonee added 나연🐹 Feature 새로운 기능 구현 및 API 연결 labels Jan 20, 2026
@y-eonee y-eonee linked an issue Jan 20, 2026 that may be closed by this pull request
1 task
@coderabbitai
Copy link

coderabbitai bot commented Jan 20, 2026

📝 Walkthrough

Walkthrough

캘린더 관련 엔드포인트·DTO·리포지토리·도메인 엔티티·유스케이스·뷰모델·뷰 및 DI를 async/throws 기반으로 재구성하고, 날짜 유틸·에셋·프로젝트 설정 참조 및 일부 프리뷰를 정리했습니다.

Changes

Cohort / File(s) 변경 사항 요약
빌드 설정
Cherrish-iOS/Cherrish-iOS.xcodeproj/project.pbxproj
Debug/Release XCBuildConfiguration에 Core/Config.xcconfig 참조(anchor + relativePath) 추가
데이터 레이어 — DI
Cherrish-iOS/Cherrish-iOS/Data/DataDependencyAssembler.swift
CalendarInterface 등록을 MockCalendarRepository()DefaultCalendarRepository(networkService:userDefaultService:)로 변경
데이터 레이어 — 네트워크 엔드포인트
Cherrish-iOS/Cherrish-iOS/Data/Network/EndPoint/CalendarAPI.swift
CalendarAPI enum 추가(월간/일간/다운타임) 및 EndPoint 구현(경로·쿼리·헤더 등)
데이터 레이어 — DTOs
Cherrish-iOS/Cherrish-iOS/Data/Model/Calendar/*
CalendarDailyResponseDTO.swift, CalendarDowntimeResponseDTO.swift, CalendarMonthlyResponseDTO.swift
Event.id → userProcedureId 네이밍 변경, 월간·다운타임 DTO 추가 및 toEntity() 매핑(-> MonthlyEntity, ProcedureDowntimeEntity, [DailyProcedureEntity])
데이터 레이어 — 불필요 파일 제거
Cherrish-iOS/Cherrish-iOS/Data/Network/EndPoint/TestAPI.swift
TestAPI 파일 삭제
데이터 레이어 — 리포지토리 변경
Cherrish-iOS/Cherrish-iOS/Data/Repository/CalendarRepository.swift
Default/MockCalendarRepository 메서드를 async/throws로 전환하고 반환 타입을 MonthlyEntity, [DailyProcedureEntity], ProcedureDowntimeEntity로 변경; 의존성 주입 추가
도메인 레이어 — 엔티티 추가/수정
Cherrish-iOS/Cherrish-iOS/Domain/Model/*
DailyProcedureEntity.swift, MonthlyEntity.swift, ProcedureDowntimeEntity.swift
DailyProcedureEntity, MonthlyEntity 추가, ProcedureEntityProcedureDowntimeEntity로 이름/필드 조정(복구일자 등)
도메인 레이어 — 인터페이스/유스케이스 변경
Cherrish-iOS/Cherrish-iOS/Domain/Interface/CalendarInterface.swift
Domain/UseCase/*
CalendarInterface를 async/throws로 갱신; 기존 FetchTodayProcedureList 제거 후 FetchTodayProcedureListUseCase 추가; FetchProcedureDowntimeUseCase 추가; FetchProcedureCountOfMonth 반환 MonthlyEntity로 변경
도메인 레이어 — DI 업데이트
Cherrish-iOS/Cherrish-iOS/Domain/DomainDependencyAssembler.swift
새 유스케이스 등록(FetchTodayProcedureListUseCase, FetchProcedureDowntimeUseCase) 및 의존성 해상 로직 조정
프레젠테이션 — Calendar UI/VM 변경
Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/*
CalendarView.swift, CalendarViewModel.swift, CalendarCellView.swift
.task(id:) 사용, PreferenceKey 기반 스크롤 오프셋 처리 도입, isDDay, treatmentDate, selectedDowntime 등 상태/타입 변경, 다운타임 비동기 fetch 추가 및 호출부 변경
프레젠테이션 — DI 업데이트
Cherrish-iOS/Cherrish-iOS/Presentation/PresentationDependencyAssembler.swift
FetchTodayProcedureListUseCaseFetchProcedureDowntimeUseCase 해상 추가, CalendarViewModel 초기화 인자에 downtime use case 포함
프레젠테이션 — 확장·유틸 정리
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/*
Date+.swift, String+.swift, String+DateFormatting.swift
전역 Date.dateFormatter 추가(Asia/Seoul), toDateString() 리팩터링; 중복된 toKoreanMonthDay 구현 정리(파일 제거/통합)
프레젠테이션 — 프리뷰 정리
Cherrish-iOS/Cherrish-iOS/Presentation/Feature/MyPage/MyPageView.swift, Presentation/Global/Components/MonthPlan.swift
PreviewProvider 블록 제거
리소스
Cherrish-iOS/Cherrish-iOS/Resources/Assets.xcassets/*
D-day 아이콘 및 일부 asset 폴더(Contents.json) 추가/정리

Sequence Diagram(s)

sequenceDiagram
    participant UI as CalendarView
    participant VM as CalendarViewModel
    participant UseCase as FetchTodayProcedureListUseCase
    participant Repo as CalendarRepository
    participant API as CalendarAPI
    participant DTO as DTO Mapper

    UI->>VM: 요청: fetchTodayProcedureList(date)
    VM->>UseCase: execute(date)
    UseCase->>Repo: fetchTodayProcedureList(date)
    Repo->>API: CalendarAPI.daily(userID, date)
    API-->>Repo: CalendarDailyResponseDTO
    Repo->>DTO: response.toEntity()
    DTO-->>Repo: [DailyProcedureEntity]
    Repo-->>UseCase: [DailyProcedureEntity]
    UseCase-->>VM: [DailyProcedureEntity]
    VM-->>UI: 갱신: procedureList, treatmentDate
Loading
sequenceDiagram
    participant UI as CalendarView
    participant VM as CalendarViewModel
    participant UseCase as FetchProcedureDowntimeUseCase
    participant Repo as CalendarRepository
    participant API as CalendarAPI
    participant DTO as DTO Mapper

    UI->>VM: 사용자 액션: 선택된 프로시저 tap -> fetchDowntimeByDay(procedureId)
    VM->>UseCase: execute(procedureId)
    UseCase->>Repo: fetchProcedureDowntime(id)
    Repo->>API: CalendarAPI.downtime(userID, id)
    API-->>Repo: CalendarDowntimeResponseDTO
    Repo->>DTO: response.toEntity()
    DTO-->>Repo: ProcedureDowntimeEntity
    Repo-->>UseCase: ProcedureDowntimeEntity
    UseCase-->>VM: ProcedureDowntimeEntity
    VM-->>UI: 갱신: selectedDowntime, downtimeByDay
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • sum130
  • wotjs020708
🚥 Pre-merge checks | ✅ 3 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning 대부분의 변경사항이 캘린더 API 연결과 관련되어 있으나, 프리뷰 블록 제거(MyPageView, MonthPlan) 및 자산 카탈로그 추가(navi_icon, onboarding)는 API 연결과 무관한 변경사항입니다. 프리뷰 블록 제거와 자산 카탈로그 추가는 별도의 이슈로 분리하거나 현재 PR에서 제거하세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 캘린더 API 연결이라는 주요 변경 사항을 명확하게 요약하고 있으며, 파일 목록이나 모호한 용어 없이 간결하고 구체적입니다.
Description check ✅ Passed PR 설명은 캘린더 API 연결 작업 내용과 관련이 있으며, 연결된 이슈 #99를 명시하고 변경사항을 설명하고 있습니다.
Linked Issues check ✅ Passed 캘린더 API 연결 구현이 완료되었습니다. 신규 DTO, 엔드포인트, 리포지토리, 유스케이스, 도메인 모델 및 뷰모델 변경을 통해 월간/일일/다운타임 데이터 조회 API가 통합되었습니다.

✏️ 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 feat/#99-캘린더-api

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

❤️ Share

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 18

Caution

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

⚠️ Outside diff range comments (2)
Cherrish-iOS/Cherrish-iOS/Domain/Model/ProcedureDowntimeEntity.swift (1)

10-17: 엔티티 구조 적절함

ProcedureDowntimeEntity로 이름 변경하여 역할이 명확해졌고, Hashable 준수로 SwiftUI 리스트에서 활용하기 좋습니다.

recoveryTargetDateString 타입인데, 도메인 레이어에서는 Date 타입을 사용하고 변환 로직을 DTO 레이어에 두는 것이 타입 안전성 측면에서 더 나을 수 있습니다. 현재 구조로도 동작에는 문제없으나, 향후 날짜 비교/연산이 필요할 경우 고려해 보세요.

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

20-46: 캘린더 DI 등록이 다른 리포지토리 실패에 의해 스킵될 수 있습니다.
HomeInterfaceTreatmentInterface가 미등록이면 조립이 조기 반환되어 캘린더 유스케이스까지 등록되지 않습니다. 해당 의존성이 항상 보장되는지 확인하고, 필요하면 등록을 분리해 주세요.

🔧 개선 예시
-        guard let homeRepository = DIContainer.shared.resolve(type: HomeInterface.self) else {
-            return
-        }
-        
-        guard let treatmentCategoryRepository = DIContainer.shared.resolve(type: TreatmentInterface.self) else {
-            return
-        }
+        let homeRepository = DIContainer.shared.resolve(type: HomeInterface.self)
+        let treatmentCategoryRepository = DIContainer.shared.resolve(type: TreatmentInterface.self)
@@
-        DIContainer.shared.register(type: FetchDashboardData.self) {
-            return DefaultFetchDashboardData(repository: homeRepository)
-        }
+        if let homeRepository {
+            DIContainer.shared.register(type: FetchDashboardData.self) {
+                return DefaultFetchDashboardData(repository: homeRepository)
+            }
+        }
@@
-        DIContainer.shared.register(type: FetchTreatmentCategoriesUseCase.self) {
-            return DefaultFetchTreatmentCategoriesUseCase(repository: treatmentCategoryRepository)
-        }
+        if let treatmentCategoryRepository {
+            DIContainer.shared.register(type: FetchTreatmentCategoriesUseCase.self) {
+                return DefaultFetchTreatmentCategoriesUseCase(repository: treatmentCategoryRepository)
+            }
+        }
🤖 Fix all issues with AI agents
In `@Cherrish-iOS/Cherrish-iOS.xcodeproj/project.pbxproj`:
- Around line 275-276: 프로젝트가 참조하는 Core/Config.xcconfig가 리포지토리에 없어 빌드가 실패하므로,
project.pbxproj에서 사용된 baseConfigurationReferenceRelativePath (값:
Core/Config.xcconfig) 및 관련 baseConfigurationReferenceAnchor 항목들을 찾아 해당 xcconfig
파일을 실제로 추가하거나(파일명: Config.xcconfig 위치: Cherrish-iOS/Core) 참조를 제거/올바른 경로로
업데이트하세요; 관련 식별자: baseConfigurationReferenceRelativePath,
baseConfigurationReferenceAnchor, Config.xcconfig,
Cherrish-iOS.xcodeproj/project.pbxproj.

In `@Cherrish-iOS/Cherrish-iOS/Data/DataDependencyAssembler.swift`:
- Around line 19-25: The DIContainer.shared.register closures in
DataDependencyAssembler capture self strongly causing retain cycles; update each
register call (for CalendarInterface, HomeInterface, TreatmentInterface) to
capture self weakly (e.g., use { [weak self] in guard let self = self else {
return nil as <Interface>? } ... } ) and return the corresponding repository
(e.g., DefaultCalendarRepository using self.networkService and
self.userDefaultService) so the assembler is not strongly retained by the
DIContainer; alternatively use [unowned self] if assembler truly lives for the
app lifetime.

In
`@Cherrish-iOS/Cherrish-iOS/Data/Model/Calendar/CalendarDowntimeResponseDTO.swift`:
- Around line 10-18: The DTO declares scheduledAt in CalendarDowntimeResponseDTO
but it is not mapped in toEntity(); either map it into ProcedureDowntimeEntity
or explicitly document why it's ignored. Fix by updating ProcedureDowntimeEntity
to include a scheduledAt property (matching type String or Date as used
elsewhere) and add the mapping inside CalendarDowntimeResponseDTO.toEntity() to
set entity.scheduledAt = scheduledAt, or if omission is intentional add a clear
comment above the scheduledAt declaration and/or a brief TODO in toEntity()
noting why it is omitted.

In
`@Cherrish-iOS/Cherrish-iOS/Data/Model/Calendar/CalendarMonthlyResponseDTO.swift`:
- Around line 14-21: In CalendarMonthlyResponseDTO.toEntity() the Int(entry.key)
conversion silently skips non-numeric keys; update this function to log a
warning when Int(entry.key) fails so malformed API keys are detectable — inside
the reduce over dailyProcedureCounts, when guard let key = Int(entry.key) fails,
call your app logging facility (e.g., Logger, os_log, or a provided process
logger) with context including the raw entry.key and entry.value before
continuing; ensure MonthlyEntity initialization remains unchanged and only the
conversion branch adds the warning.

In `@Cherrish-iOS/Cherrish-iOS/Data/Network/EndPoint/CalendarAPI.swift`:
- Around line 51-56: The parameterEncoding computed property in CalendarAPI
currently returns JSONEncoding() for the .monthly, .daily, and .downtime cases
which sends params in the HTTP body (wrong for GET); change it to return
URLEncoding.queryString so query parameters are encoded in the URL for these GET
endpoints (update the parameterEncoding var in the CalendarAPI type and ensure
the switch cases .monthly, .daily, .downtime return URLEncoding.queryString).

In `@Cherrish-iOS/Cherrish-iOS/Data/Repository/CalendarRepository.swift`:
- Around line 14-19: The initializer in CalendarRepository is ignoring the
injected dependencies by always instantiating DefaultNetworkService and
DefaultUserDefaultService; change the init to assign the passed parameters to
the properties (self.networkService = networkService, self.userDefaultService =
userDefaultService) so DI works correctly, or if you want default fallbacks
provide default parameter values in the init signature (e.g., networkService:
NetworkService = DefaultNetworkService()) and still assign the parameters to the
properties; update the initializer that currently references
DefaultNetworkService and DefaultUserDefaultService accordingly.
- Around line 64-90: The two mock methods do not match the protocol signature;
update fetchProcedureCountOfMonth and fetchTodayProcedureList in
MockCalendarRepository to have the same async throws signature as the
CalendarInterface (i.e., declare them as async throws and keep their current
return types MonthlyEntity and [DailyProcedureEntity]) and ensure their
implementations either return the mock data or throw appropriate errors if
needed so the signatures compile against the protocol.

In `@Cherrish-iOS/Cherrish-iOS/Domain/Model/DailyProcedureEntity.swift`:
- Around line 10-15: DailyProcedureEntity의 type 프로퍼티가 현재 String으로 되어 있는데 시술 유형이
정해진 값들이면 enum으로 변경해 타입 안전성 및 자동완성을 얻을 수 있습니다; DailyProcedureEntity의 type을
String에서 적절한 enum 타입(예: ProcedureType)으로 대체하고, 필요한 경우 Codable/Hashable 채택과
rawValue 기반 초기화 또는 API 응답을 안전하게 처리할 수 있도록 init(from:) 변환 로직(또는 failable
initializer)을 추가해 예측 불가능한 값도 처리하도록 구현하세요.

In `@Cherrish-iOS/Cherrish-iOS/Domain/Model/MonthlyEntity.swift`:
- Around line 10-12: MonthlyEntity.dailyProcedureCounts uses Int keys that
represent the calendar day but lacks documentation; add a brief doc comment
above the MonthlyEntity struct and the dailyProcedureCounts property clarifying
that the dictionary key is the day-of-month (1...31) and what the value
represents (procedure count), or introduce a small typealias (e.g., typealias
Day = Int) and change the property signature to dailyProcedureCounts: [Day: Int]
with the same doc comment to make intent explicit; update any initializer/usage
to use the Day alias if you add it.

In `@Cherrish-iOS/Cherrish-iOS/Domain/UseCase/FetchProcedureCountOfMonth.swift`:
- Around line 10-12: Rename the protocol FetchProcedureCountOfMonth to
FetchProcedureCountOfMonthUseCase for naming consistency; update the protocol
declaration and all references/usages (e.g., any variables, dependency
injections, type annotations, and conforming types) to the new name so
FetchProcedureCountOfMonthUseCase replaces FetchProcedureCountOfMonth across the
codebase, and ensure any implementations (conforming structs/classes) update
their protocol conformance accordingly.

In
`@Cherrish-iOS/Cherrish-iOS/Domain/UseCase/FetchTodayProcedureListUseCase.swift`:
- Around line 10-14: Rename the implementation to match the protocol and file:
change struct DefaultFetchTodayProcedureUseCase to
DefaultFetchTodayProcedureListUseCase and rename the file to
FetchTodayProcedureListUseCase.swift; update all references/usages
(initializers, type annotations, imports, tests) to the new struct name so
DefaultFetchTodayProcedureListUseCase conforms to
FetchTodayProcedureListUseCase.

In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarCellView.swift`:
- Line 26: The isDDay property on CalendarCellView should be made immutable:
change the declaration from var isDDay: Bool to let isDDay: Bool in the
CalendarCellView struct/class (ensure any initializer assigns it), remove any
internal assignments to isDDay, and update any external call sites that relied
on mutating it to instead create a new instance or pass the value via the
initializer; verify there are no places setting isDDay after init (search for
"isDDay" usages) and update accordingly.

In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarView.swift`:
- Around line 50-58: The current .task(id: viewModel.currentMonth) block guards
execution with `if calendarMode == .none`, preventing
`fetchProcedureCountsOfMonth()` from running when the month changes while in
`.selectedProcedure` mode; remove that guard so that inside the `.task(id:
viewModel.currentMonth)` you always call `try await
viewModel.fetchProcedureCountsOfMonth()` (and still call `try await
viewModel.fetchTodayProcedureList()` as needed), keeping the existing do/catch
and the `CherrishLogger.error(error)` handling so month changes always trigger
data refresh regardless of `calendarMode`.

In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarViewModel.swift`:
- Around line 123-126: getCurrentMonth creates a gregorian Calendar fixed to
TimeZone(identifier: "Asia/Seoul") while other helpers (getYearAndMonthString,
extractDate, firstDateOfCurrentMonth) use Calendar.current, which can cause
inconsistent month/day calculations on devices in different time zones; update
the code to use a single shared Calendar instance (e.g., create a private var
kstCalendar = Calendar(identifier: .gregorian) with timeZone =
TimeZone(identifier: "Asia/Seoul") and replace usages of Calendar.current in
getYearAndMonthString, extractDate, firstDateOfCurrentMonth (and any other date
helpers) to use kstCalendar or pass the calendar as a parameter so all date math
uses the same calendar/timeZone consistently.
- Around line 110-112: Do not set treatmentDate before fetching — fetch the
procedures first and only update state on success: call
fetchTodayProcedureListUseCase.execute(date: selectedDate.toDateString()) into a
local/temp variable, then on success assign procedureList = fetchedValue and
treatmentDate = selectedDate.toDateString(); on failure leave
treatmentDate/procedureList unchanged and handle/log the error; update the code
around treatmentDate, procedureList and fetchTodayProcedureListUseCase.execute
accordingly.

In `@Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/Date`+.swift:
- Around line 25-32: The static DateFormatter named dateFormatter in extension
Date creates ambiguity with the similarly named dateFormatter() in
String+.swift; please rename the symbol (e.g., isoDateFormatter or
koreanDateFormatter) to a more specific identifier and update all usages to the
new name (reference: Date.dateFormatter) so callers no longer collide or get
confused with String+.dateFormatter().

In `@Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/String`+.swift:
- Around line 33-42: The toKoreanMonthDay computed property should reuse static
DateFormatters with explicit locale and timeZone to avoid per-call allocation
and day-shift bugs: create two static formatters (e.g., isoFormatter and
koreanFormatter) where isoFormatter is configured with dateFormat "yyyy-MM-dd",
locale = Locale(identifier: "en_US_POSIX") and timeZone =
TimeZone(secondsFromGMT: 0) for stable parsing, and koreanFormatter is
configured with dateFormat "M월 d일", locale = Locale(identifier: "ko_KR") and an
appropriate timeZone (e.g., TimeZone.current); then replace the local
DateFormatter in String.toKoreanMonthDay to parse with isoFormatter and format
with koreanFormatter.
- Around line 11-15: The inputFormatter DateFormatter uses a locale of "ko_KR"
but fixed-format parsing must use Locale(identifier: "en_US_POSIX"); update the
static inputFormatter initializer (inputFormatter) to set dateFormatter.locale =
Locale(identifier: "en_US_POSIX") and also set a stable calendar/timeZone (e.g.,
dateFormatter.calendar = Calendar(identifier: .gregorian) and
dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)) to ensure deterministic
parsing of "yyyy-MM-dd".

Comment on lines +275 to +276
baseConfigurationReferenceAnchor = 51B5D31A2F0514390090B8B4 /* Cherrish-iOS */;
baseConfigurationReferenceRelativePath = Core/Config.xcconfig;
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:

#!/bin/bash

# 1) Search for Config.xcconfig in the repository
echo "=== Searching for Config.xcconfig ==="
fd -a 'Config\.xcconfig$' 2>/dev/null || echo "No Config.xcconfig found"

# 2) Check the expected path
echo ""
echo "=== Checking expected path ==="
if [ -f "Cherrish-iOS/Core/Config.xcconfig" ]; then
    echo "✓ File exists at Cherrish-iOS/Core/Config.xcconfig"
    wc -l "Cherrish-iOS/Core/Config.xcconfig"
else
    echo "✗ File NOT found at Cherrish-iOS/Core/Config.xcconfig"
fi

# 3) List contents of Cherrish-iOS/Core directory if it exists
echo ""
echo "=== Contents of Cherrish-iOS/Core ==="
if [ -d "Cherrish-iOS/Core" ]; then
    ls -la "Cherrish-iOS/Core/" 2>/dev/null || echo "Directory exists but cannot list contents"
else
    echo "Directory Cherrish-iOS/Core does not exist"
fi

# 4) Check pbxproj references to Config.xcconfig
echo ""
echo "=== References to Config.xcconfig in pbxproj ==="
rg -n "Config\.xcconfig" "Cherrish-iOS/Cherrish-iOS.xcodeproj/project.pbxproj" 2>/dev/null || echo "No references found"

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 605


Config.xcconfig 파일이 누락되어 빌드 실패

Core/Config.xcconfig 파일이 리포지토리에 존재하지 않습니다. Cherrish-iOS/Core 디렉토리 자체도 없으며, 프로젝트 파일에서는 4개 라인(153, 218, 276, 314)에서 이 파일을 참조하고 있습니다. 이 설정 파일을 생성하거나 참조를 제거해야 빌드가 가능합니다.

🤖 Prompt for AI Agents
In `@Cherrish-iOS/Cherrish-iOS.xcodeproj/project.pbxproj` around lines 275 - 276,
프로젝트가 참조하는 Core/Config.xcconfig가 리포지토리에 없어 빌드가 실패하므로, project.pbxproj에서 사용된
baseConfigurationReferenceRelativePath (값: Core/Config.xcconfig) 및 관련
baseConfigurationReferenceAnchor 항목들을 찾아 해당 xcconfig 파일을 실제로 추가하거나(파일명:
Config.xcconfig 위치: Cherrish-iOS/Core) 참조를 제거/올바른 경로로 업데이트하세요; 관련 식별자:
baseConfigurationReferenceRelativePath, baseConfigurationReferenceAnchor,
Config.xcconfig, Cherrish-iOS.xcodeproj/project.pbxproj.

Comment on lines 19 to 25
func assemble() {
DIContainer.shared.register(type: CalendarInterface.self) {
return MockCalendarRepository()
return DefaultCalendarRepository(
networkService: self.networkService,
userDefaultService: self.userDefaultService
)
}
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:

#!/bin/bash
# Search for DIContainer class definition and implementation
fd -e swift | head -20

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 1319


🏁 Script executed:

#!/bin/bash
# Find DIContainer definition
rg -n "class DIContainer" --type swift -A 20

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 1700


🏁 Script executed:

#!/bin/bash
# Find DataDependencyAssembler usage patterns
rg -n "DataDependencyAssembler" --type swift -B 2 -A 5

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 2193


🏁 Script executed:

#!/bin/bash
# Check the register method signature and other uses
rg -n "\.register\(" --type swift -B 1 -A 3 | head -100

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 7271


🏁 Script executed:

#!/bin/bash
# Get full DataDependencyAssembler.swift
cat -n Cherrish-iOS/Cherrish-iOS/Data/DataDependencyAssembler.swift

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 1302


🏁 Script executed:

#!/bin/bash
# Check the app initialization to confirm DataDependencyAssembler is only used during startup
cat -n Cherrish-iOS/Cherrish-iOS/App/Cherrish_iOSApp.swift

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 538


DIContainer 클로저에서 self 강한 참조로 인한 메모리 누수 발생

DIContainer.shared.register() 호출 시 클로저가 self를 강하게 캡처하고 있습니다. DIContainer는 싱글톤이고 dependencies 딕셔너리가 앱 전체 생명주기 동안 유지되므로, 각 클로저는 DataDependencyAssembler 인스턴스에 대한 강한 참조를 계속 유지하게 됩니다. 이로 인해 retain cycle이 발생하여 assembler가 메모리에서 해제되지 않습니다.

모든 register() 호출(CalendarInterface, HomeInterface, TreatmentInterface)을 다음과 같이 수정하세요:

DIContainer.shared.register(type: CalendarInterface.self) { [weak self] in
    guard let self = self else { return nil as CalendarInterface? }
    return DefaultCalendarRepository(
        networkService: self.networkService,
        userDefaultService: self.userDefaultService
    )
}

또는 assembler의 생명주기가 app 시작 이후 필요 없다면 [unowned self] 사용도 고려할 수 있습니다.

🤖 Prompt for AI Agents
In `@Cherrish-iOS/Cherrish-iOS/Data/DataDependencyAssembler.swift` around lines 19
- 25, The DIContainer.shared.register closures in DataDependencyAssembler
capture self strongly causing retain cycles; update each register call (for
CalendarInterface, HomeInterface, TreatmentInterface) to capture self weakly
(e.g., use { [weak self] in guard let self = self else { return nil as
<Interface>? } ... } ) and return the corresponding repository (e.g.,
DefaultCalendarRepository using self.networkService and self.userDefaultService)
so the assembler is not strongly retained by the DIContainer; alternatively use
[unowned self] if assembler truly lives for the app lifetime.

Comment on lines +10 to +18
struct CalendarDowntimeResponseDTO: Decodable {
let userProcedureId: Int
let scheduledAt: String
let downtimeDays: Int
let recoveryTargetDate: String
let sensitiveDays: [String]
let cautionDays: [String]
let recoveryDays: [String]
}
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

scheduledAt 필드가 사용되지 않음

scheduledAt 프로퍼티(Line 12)가 선언되어 있지만 toEntity() 매핑에서 사용되지 않습니다. 의도적으로 무시하는 것이라면 주석을 추가하거나, 필요한 경우 ProcedureDowntimeEntity에 해당 필드를 추가해 주세요.

🤖 Prompt for AI Agents
In
`@Cherrish-iOS/Cherrish-iOS/Data/Model/Calendar/CalendarDowntimeResponseDTO.swift`
around lines 10 - 18, The DTO declares scheduledAt in
CalendarDowntimeResponseDTO but it is not mapped in toEntity(); either map it
into ProcedureDowntimeEntity or explicitly document why it's ignored. Fix by
updating ProcedureDowntimeEntity to include a scheduledAt property (matching
type String or Date as used elsewhere) and add the mapping inside
CalendarDowntimeResponseDTO.toEntity() to set entity.scheduledAt = scheduledAt,
or if omission is intentional add a clear comment above the scheduledAt
declaration and/or a brief TODO in toEntity() noting why it is omitted.

Comment on lines +14 to +21
extension CalendarMonthlyResponseDTO {
func toEntity() -> MonthlyEntity {
let mapped = dailyProcedureCounts.reduce(into: [Int: Int]()) { result, entry in
guard let key = Int(entry.key) else { return }
result[key] = entry.value
}
return .init(dailyProcedureCounts: mapped)
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

숫자가 아닌 키 변환 실패 시 디버깅을 위한 로깅을 고려하세요.

현재 구현은 Int(entry.key)가 실패할 경우 해당 엔트리를 무시합니다. API 응답 데이터 문제를 조기에 발견하기 위해 경고 로그를 추가하는 것을 권장합니다.

♻️ 제안된 수정
 func toEntity() -> MonthlyEntity {
     let mapped = dailyProcedureCounts.reduce(into: [Int: Int]()) { result, entry in
-        guard let key = Int(entry.key) else { return }
+        guard let key = Int(entry.key) else {
+            CherrishLogger.warning("Invalid day key in monthly response: \(entry.key)")
+            return
+        }
         result[key] = entry.value
     }
     return .init(dailyProcedureCounts: mapped)
 }
🤖 Prompt for AI Agents
In
`@Cherrish-iOS/Cherrish-iOS/Data/Model/Calendar/CalendarMonthlyResponseDTO.swift`
around lines 14 - 21, In CalendarMonthlyResponseDTO.toEntity() the
Int(entry.key) conversion silently skips non-numeric keys; update this function
to log a warning when Int(entry.key) fails so malformed API keys are detectable
— inside the reduce over dailyProcedureCounts, when guard let key =
Int(entry.key) fails, call your app logging facility (e.g., Logger, os_log, or a
provided process logger) with context including the raw entry.key and
entry.value before continuing; ensure MonthlyEntity initialization remains
unchanged and only the conversion branch adds the warning.

Comment on lines +110 to 112
treatmentDate = selectedDate.toDateString()
procedureList = try await fetchTodayProcedureListUseCase.execute(date: treatmentDate)
}
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

실패 시 상태 불일치 가능—업데이트 순서 조정 필요.
Line 110에서 날짜를 먼저 갱신하면, fetch 실패 시 treatmentDate만 바뀌고 procedureList는 이전 값으로 남습니다. 성공 후에 함께 업데이트하는 편이 안전합니다.

🔧 제안 수정안
 `@MainActor`
 func fetchTodayProcedureList() async throws {
-    treatmentDate = selectedDate.toDateString()
-    procedureList = try await fetchTodayProcedureListUseCase.execute(date: treatmentDate)
+    let dateString = selectedDate.toDateString()
+    let list = try await fetchTodayProcedureListUseCase.execute(date: dateString)
+    treatmentDate = dateString
+    procedureList = list
 }
🤖 Prompt for AI Agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarViewModel.swift`
around lines 110 - 112, Do not set treatmentDate before fetching — fetch the
procedures first and only update state on success: call
fetchTodayProcedureListUseCase.execute(date: selectedDate.toDateString()) into a
local/temp variable, then on success assign procedureList = fetchedValue and
treatmentDate = selectedDate.toDateString(); on failure leave
treatmentDate/procedureList unchanged and handle/log the error; update the code
around treatmentDate, procedureList and fetchTodayProcedureListUseCase.execute
accordingly.

Comment on lines 11 to 15
private static let inputFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.locale = Locale(identifier: "ko_KR")
return dateFormatter
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:

find . -name "String+.swift" -o -name "*String*.swift" | head -20

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 141


🏁 Script executed:

cd Cherrish-iOS && find . -path "*Presentation/Global/Extension*" -type f

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 812


🏁 Script executed:

cat -n "Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/String+.swift" | head -60

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 1536


🏁 Script executed:

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

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 1881


🌐 Web query:

iOS DateFormatter locale en_US_POSIX vs ko_KR yyyy-MM-dd parsing behavior

💡 Result:

Short answer

  • en_US_POSIX with a fixed dateFormat (e.g. "yyyy-MM-dd") produces stable, predictable parsing (Gregorian calendar, no user overrides). Use it for machine/interop formats. [1][2]
  • Using ko_KR (or other user/locales) can change the formatter’s calendar, era and format interpretation (and can be affected by user settings like 24‑hour/AM‑PM or regional calendar rules), which can make "yyyy-MM-dd" parse incorrectly or yield the wrong year/day. Set the locale to en_US_POSIX (and explicitly the gregorian calendar/timeZone) to avoid this. [1][2][3]

Why this happens (brief)

  • Apple’s QA (QA1480) and docs: en_US_POSIX is the POSIX locale intended for fixed-format parsing; other locales may alter parsing behavior/format string interpretation. [1][2]
  • Practical reports show wrong years/offsets when the locale/calendar is not fixed (e.g. device locale like ko_KR or modified en_US variants). For parsing you should set locale = Locale(identifier: "en_US_POSIX") and, if needed, formatter.calendar = Calendar(identifier: .gregorian) and formatter.timeZone = TimeZone(secondsFromGMT: 0). [3][1]

References

  • Apple Technical Q&A QA1480 (NSDateFormatter and Internet Dates). [1]
  • Apple Date Formatters docs / Data Formatting guide. [2]
  • Examples / community reports (Stack Overflow showing wrong-year behavior and recommendation to use en_US_POSIX). [3]

🌐 Web query:

DateFormatter parsing fixed format yyyy-MM-dd locale impact calendar

💡 Result:

Short answer: don’t rely on the default locale/calendar. For a fixed-format string like "yyyy‑MM‑dd" set the formatter’s locale to en_US_POSIX and its calendar (usually Gregorian) — otherwise a user-chosen calendar (Buddhist, Hebrew, etc.) or locale preferences can change how the year/month/day are interpreted. Also set timeZone if you need a deterministic midnight instant. Example:

let f = DateFormatter()
f.locale = Locale(identifier: "en_US_POSIX") // fixes locale-related rewriting
f.calendar = Calendar(identifier: .gregorian) // avoids user-selected calendar math
f.timeZone = TimeZone(secondsFromGMT: 0) // optional — makes parsed Date deterministic
f.dateFormat = "yyyy-MM-dd"
let date = f.date(from: "2026-01-20")

Why: Apple’s docs and QA note explain that format strings are still affected by the user’s locale and calendar and recommend en_US_POSIX for fixed internet/formatted dates. [1][2]

Sources:
[1] Apple Technical Q&A QA1480 (NSDateFormatter and Internet Dates).
[2] Apple Date Formatters documentation (NSDateFormatter behavior and calendars).


고정 포맷 파싱은 반드시 en_US_POSIX를 사용해야 합니다.

Apple의 공식 권장사항(QA1480)에 따르면, "yyyy-MM-dd" 같은 고정 포맷 문자열 파싱은 en_US_POSIX 로케일을 반드시 사용해야 합니다. 현재 ko_KR 로케일은 사용자의 캘린더 설정(예: 불교력, 히브리력) 및 지역 규칙에 따라 연/월/일 해석이 변경되어 파싱 실패 또는 잘못된 날짜 변환을 초래할 수 있습니다.

🔧 제안 수정
 private static let inputFormatter: DateFormatter = {
     let dateFormatter = DateFormatter()
     dateFormatter.dateFormat = "yyyy-MM-dd"
-    dateFormatter.locale = Locale(identifier: "ko_KR")
+    dateFormatter.locale = Locale(identifier: "en_US_POSIX")
+    dateFormatter.calendar = Calendar(identifier: .gregorian)
     return dateFormatter
 }()
📝 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
private static let inputFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.locale = Locale(identifier: "ko_KR")
return dateFormatter
private static let inputFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.calendar = Calendar(identifier: .gregorian)
return dateFormatter
}()
🤖 Prompt for AI Agents
In `@Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/String`+.swift around
lines 11 - 15, The inputFormatter DateFormatter uses a locale of "ko_KR" but
fixed-format parsing must use Locale(identifier: "en_US_POSIX"); update the
static inputFormatter initializer (inputFormatter) to set dateFormatter.locale =
Locale(identifier: "en_US_POSIX") and also set a stable calendar/timeZone (e.g.,
dateFormatter.calendar = Calendar(identifier: .gregorian) and
dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)) to ensure deterministic
parsing of "yyyy-MM-dd".

Comment on lines +33 to +42
var toKoreanMonthDay: String {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"

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

formatter.dateFormat = "M월 d일"
return formatter.string(from: parsedDate)
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

toKoreanMonthDay에서 포맷터 재사용 + 설정 일관성을 맞춰주세요.

매 호출마다 DateFormatter를 생성하고 locale/timeZone을 지정하지 않아 기존 공유 포맷터와 설정 불일치가 생길 수 있습니다(특히 날짜 하루 밀림 이슈 재발 가능). 정적 포맷터로 재사용하면서 설정을 일치시키는 편이 안전합니다.

♻️ 제안 리팩터
 private static let outputFormatter: DateFormatter = {
     let dateFormatter = DateFormatter()
     dateFormatter.dateFormat = "M월 d일 EEEE"
     dateFormatter.locale = Locale(identifier: "ko_KR")
     return dateFormatter
 }()
+
+private static let monthDayFormatter: DateFormatter = {
+    let dateFormatter = DateFormatter()
+    dateFormatter.dateFormat = "M월 d일"
+    dateFormatter.locale = Locale(identifier: "ko_KR")
+    return dateFormatter
+}()
@@
 var toKoreanMonthDay: String {
-    let formatter = DateFormatter()
-    formatter.dateFormat = "yyyy-MM-dd"
-    
-    guard let parsedDate = formatter.date(from: self) else {
+    guard let parsedDate = String.inputFormatter.date(from: self) else {
         return self
     }
-    
-    formatter.dateFormat = "M월 d일"
-    return formatter.string(from: parsedDate)
+    return String.monthDayFormatter.string(from: parsedDate)
 }
🤖 Prompt for AI Agents
In `@Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/String`+.swift around
lines 33 - 42, The toKoreanMonthDay computed property should reuse static
DateFormatters with explicit locale and timeZone to avoid per-call allocation
and day-shift bugs: create two static formatters (e.g., isoFormatter and
koreanFormatter) where isoFormatter is configured with dateFormat "yyyy-MM-dd",
locale = Locale(identifier: "en_US_POSIX") and timeZone =
TimeZone(secondsFromGMT: 0) for stable parsing, and koreanFormatter is
configured with dateFormat "M월 d일", locale = Locale(identifier: "ko_KR") and an
appropriate timeZone (e.g., TimeZone.current); then replace the local
DateFormatter in String.toKoreanMonthDay to parse with isoFormatter and format
with koreanFormatter.

@y-eonee y-eonee force-pushed the feat/#99-캘린더-api branch from 0ca3cc5 to 2b4e6ec Compare January 20, 2026 14:18
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: 2

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/Presentation/PresentationDependencyAssembler.swift (1)

40-42: 에러 처리의 일관성이 부족합니다.

FetchDashboardData resolve 실패 시 로깅 없이 단순 return하고 있어, 위의 lines 28-29에서 CherrishLogger.error(CherrishError.DIFailedError)를 호출하는 패턴과 일관성이 없습니다.

수정 제안
         guard let fetchDashboardData = DIContainer.shared.resolve(type: FetchDashboardData.self) else {
+            CherrishLogger.error(CherrishError.DIFailedError)
             return
         }
🤖 Fix all issues with AI agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarView.swift`:
- Line 113: The call passing selectedProcedureID ?? 0 can accidentally treat 0
as a valid procedure ID; update the call to avoid a potentially valid fallback
by either passing the Optional directly (selectedProcedureID) to isDDay or using
an explicit invalid sentinel like -1 (e.g., selectedProcedureID ?? -1) and
ensure isDDay(_:selectedProcedureID:) handles an optional/invalid ID
accordingly; locate the call site and the isDDay function to make the
signature/handling consistent.
- Around line 177-189: The tap handler can start overlapping Tasks causing stale
data; add a cancellable Task property (e.g., a `@State` private var fetchTask) and
in the onTapGesture cancel any existing fetchTask before creating a new one,
then assign fetchTask = Task { ... } to run
viewModel.fetchDowntimeByDay(procedureId:), handle CancellationError separately,
and after awaiting verify selectedProcedureID (and/or compare
procedure.procedureId) before applying any result to selectedDowntime or other
UI state; this ensures previous fetches are cancelled and results are only
applied if still the current selection.
♻️ Duplicate comments (1)
Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarView.swift (1)

50-58: 월 변경 시 데이터 갱신이 반드시 필요합니다.

현재 코드에서 calendarMode == .none 가드로 인해 시술 상세 모드(.selectedProcedure)에서 월을 변경해도 fetchProcedureCountsOfMonth()가 실행되지 않습니다. 달력 그리드가 각 날짜의 시술 개수를 표시해야 하므로 모드와 관계없이 월 변경 시 항상 데이터를 갱신해야 합니다.

procedureCount: viewModel.getProcedureCount(for: value),
isSelected: viewModel.isSelected(value),
downtimeState: viewModel.getDowntimeState(for: value.date),
isDDay: viewModel.isDDay(for: value.date, selectedProcedureID: selectedProcedureID ?? 0),
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

selectedProcedureID ?? 0 사용 시 잠재적 문제가 있습니다.

selectedProcedureIDnil일 때 fallback 값으로 0을 사용하고 있는데, 만약 0이 유효한 procedure ID라면 의도치 않게 D-Day가 표시될 수 있습니다. isDDay 함수 내부에서 guard 처리가 되어 있다면 문제없지만, 명시적으로 -1 같은 무효한 값을 사용하거나 Optional을 직접 전달하는 것이 더 안전합니다.

🤖 Prompt for AI Agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarView.swift`
at line 113, The call passing selectedProcedureID ?? 0 can accidentally treat 0
as a valid procedure ID; update the call to avoid a potentially valid fallback
by either passing the Optional directly (selectedProcedureID) to isDDay or using
an explicit invalid sentinel like -1 (e.g., selectedProcedureID ?? -1) and
ensure isDDay(_:selectedProcedureID:) handles an optional/invalid ID
accordingly; locate the call site and the isDDay function to make the
signature/handling consistent.

Comment on lines 177 to 189
.onTapGesture {
calendarMode.toggle()
selectedProcedureID = procedure.procedureId
viewModel.fetchDowntimeByDay(procedureId: procedure.procedureId)

Task {
do {
try await viewModel.fetchDowntimeByDay(procedureId: procedure.procedureId)
}
catch {
CherrishLogger.error(error)
}
}
}
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

연속 탭 시 race condition 가능성이 있습니다.

사용자가 다른 procedure를 빠르게 연속으로 탭하면, 이전 fetchDowntimeByDay 요청이 완료되기 전에 새로운 요청이 시작될 수 있습니다. 이 경우 selectedDowntime이 현재 선택된 procedure와 맞지 않는 데이터로 업데이트될 수 있습니다.

수정 제안: Task 취소 처리 추가
+    `@State` private var currentDowntimeTask: Task<Void, Never>? = nil

그리고 onTapGesture 내부에서:

                         .onTapGesture {
                             calendarMode.toggle()
                             selectedProcedureID = procedure.procedureId
                             
+                            currentDowntimeTask?.cancel()
-                            Task {
+                            currentDowntimeTask = Task {
                                 do {
                                     try await viewModel.fetchDowntimeByDay(procedureId: procedure.procedureId)
-                                }
-                                catch {
+                                } catch {
+                                    if Task.isCancelled { return }
                                     CherrishLogger.error(error)
                                 }
                             }
                         }
🤖 Prompt for AI Agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarView.swift`
around lines 177 - 189, The tap handler can start overlapping Tasks causing
stale data; add a cancellable Task property (e.g., a `@State` private var
fetchTask) and in the onTapGesture cancel any existing fetchTask before creating
a new one, then assign fetchTask = Task { ... } to run
viewModel.fetchDowntimeByDay(procedureId:), handle CancellationError separately,
and after awaiting verify selectedProcedureID (and/or compare
procedure.procedureId) before applying any result to selectedDowntime or other
UI state; this ensures previous fetches are cancelled and results are only
applied if still the current selection.

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.

코드가 깔끔하네요 고생하셨씁니다!

@y-eonee y-eonee force-pushed the feat/#99-캘린더-api branch from d9b67d3 to e31983c Compare January 20, 2026 20:55
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.

수고하셨습니다~

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.

양 많았는디 깔끔함미다!! 수고하셨어요👍👍

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

🤖 Fix all issues with AI agents
In `@Cherrish-iOS/Cherrish-iOS/Domain/DomainDependencyAssembler.swift`:
- Around line 24-30: Remove the redundant dependency resolution for
TreatmentInterface: delete the unused guard that assigns
treatmentCategoryRepository (the guard using DIContainer.shared.resolve(type:
TreatmentInterface.self) at the top) and rely on the existing resolution that
assigns treatmentRepository later (the resolve for TreatmentInterface in
treatmentRepository). Ensure only one DIContainer.shared.resolve(type:
TreatmentInterface.self) remains and that all uses reference treatmentRepository
so no unused variables remain.
- Around line 40-42: Remove the duplicate resolution of HomeInterface: delete
the second guard that calls DIContainer.shared.resolve(type: HomeInterface.self)
and instead reuse the earlier homeRepository obtained on lines 24-26 (the
originally resolved variable named homeRepository) in the code paths that
follow; ensure there are no shadowed variables with the same name and that any
subsequent references use the initial homeRepository instance rather than
resolving again.

In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarView.swift`:
- Line 330: The closing brace in CalendarView.swift for the CalendarView
declaration/closure is indented with 2 spaces instead of the project's 4-space
style; locate the mismatched brace in the CalendarView (e.g., end of
struct/extension or body where the lone "}" appears) and re-indent that closing
brace to 4 spaces so it matches the rest of the file and project coding style.

In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarViewModel.swift`:
- Around line 78-91: isDDay currently creates a DateFormatter and Calendar on
every call which is costly; add cached static properties (e.g., private static
let kstDateFormatter: DateFormatter and private static let kstCalendar:
Calendar) inside CalendarViewModel configured with "yyyy-MM-dd",
Locale(identifier: "ko_KR") and TimeZone(identifier: "Asia/Seoul") and replace
the local formatter/calendar creation in isDDay(for:selectedProcedureID:) to use
Self.kstDateFormatter and Self.kstCalendar (avoid force-unwrapping
timeZone—provide a sensible fallback) while keeping the existing guard checks
for selectedDowntime and parsing the target date.
- Around line 86-87: Replace the forced unwrap of TimeZone(identifier:
"Asia/Seoul") when creating the Calendar with safe handling: obtain the timezone
using guard/if-let (e.g., guard let seoulTZ = TimeZone(identifier: "Asia/Seoul")
else { ... }) or provide a fallback (e.g., TimeZone.current) and use that value
to set calendar.timeZone; apply the same change in places referenced by isDDay
and getCurrentMonth so neither uses a forced unwrap of TimeZone(identifier:).

In `@Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/Date`+.swift:
- Around line 20-32: The getDowntimeState() method must be annotated with
`@MainActor` to ensure calls that use the shared Date.dateFormatter run on the
main thread; update the declaration of getDowntimeState() to include `@MainActor`
(same pattern used by fetchTodayProcedureList()) so the compiler enforces
main-thread execution whenever getDowntimeState() accesses Date.dateFormatter or
other UI/main-only resources.
♻️ Duplicate comments (17)
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/String+.swift (2)

11-16: 고정 포맷 파싱에는 en_US_POSIX 로케일을 사용해야 합니다.

이전 리뷰에서 지적된 사항이 아직 해결되지 않았습니다. "yyyy-MM-dd" 같은 고정 포맷 문자열 파싱 시 ko_KR 로케일을 사용하면 사용자 캘린더 설정(불교력, 히브리력 등)에 따라 날짜 해석이 달라질 수 있습니다. Apple QA1480 권장사항에 따라 en_US_POSIX로 변경해 주세요.

🔧 제안 수정
 private static let inputFormatter: DateFormatter = {
     let dateFormatter = DateFormatter()
     dateFormatter.dateFormat = "yyyy-MM-dd"
-    dateFormatter.locale = Locale(identifier: "ko_KR")
+    dateFormatter.locale = Locale(identifier: "en_US_POSIX")
+    dateFormatter.calendar = Calendar(identifier: .gregorian)
     return dateFormatter
 }()

33-43: toKoreanMonthDay에서 정적 포맷터를 재사용하고 로케일 설정을 추가해 주세요.

이전 리뷰에서 지적된 사항입니다. 현재 구현은 매 호출마다 DateFormatter를 새로 생성하여 성능 저하를 유발하며, 로케일/타임존 미설정으로 인해 파싱 동작이 일관되지 않을 수 있습니다. 기존 inputFormatter를 재사용하고 출력용 정적 포맷터를 추가하는 것이 좋습니다.

♻️ 제안 리팩터
+private static let monthDayFormatter: DateFormatter = {
+    let dateFormatter = DateFormatter()
+    dateFormatter.dateFormat = "M월 d일"
+    dateFormatter.locale = Locale(identifier: "ko_KR")
+    return dateFormatter
+}()
+
 var toKoreanMonthDay: String {
-    let formatter = DateFormatter()
-    formatter.dateFormat = "yyyy-MM-dd"
-    
-    guard let parsedDate = formatter.date(from: self) else {
+    guard let parsedDate = String.inputFormatter.date(from: self) else {
         return self
     }
-    
-    formatter.dateFormat = "M월 d일"
-    return formatter.string(from: parsedDate)
+    return String.monthDayFormatter.string(from: parsedDate)
 }
Cherrish-iOS/Cherrish-iOS.xcodeproj/project.pbxproj (1)

275-276: Config.xcconfig 경로 유효성 확인 필요

Core/Config.xcconfig가 실제로 존재하지 않거나 경로가 다르면 타깃 Debug/Release 빌드가 바로 실패합니다. 해당 파일을 리포지토리에 추가하거나 baseConfigurationReferenceRelativePath를 실제 위치로 맞춰 주세요.

다음 스크립트로 파일 존재 여부와 pbxproj 참조를 확인해 주세요:

#!/bin/bash
# Config.xcconfig 존재 여부 및 참조 확인 (read-only)

echo "=== Config.xcconfig 위치 검색 ==="
fd -a 'Config\.xcconfig$' 2>/dev/null || echo "Config.xcconfig 파일을 찾지 못했습니다."

echo ""
echo "=== pbxproj 내 참조 확인 ==="
rg -n "baseConfigurationReferenceRelativePath = Core/Config\.xcconfig" \
  Cherrish-iOS/Cherrish-iOS.xcodeproj/project.pbxproj

Also applies to: 313-314

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

10-12: 간결하고 적절한 도메인 엔티티 구조입니다.

월별 시술 횟수를 일별로 관리하는 심플한 구조로, 목적에 부합합니다.

문서화 관련 제안은 이전 리뷰에서 이미 언급되었습니다.

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

10-15: 적절한 도메인 엔티티 구조입니다.

Hashable 준수로 SwiftUI에서 활용하기 좋고, 필요한 필드들을 잘 갖추고 있습니다.

type 프로퍼티의 enum 변환 제안은 이전 리뷰에서 이미 언급되었습니다.

Cherrish-iOS/Cherrish-iOS/Data/DataDependencyAssembler.swift (1)

21-24: Mock에서 실제 Repository로 전환 완료

MockCalendarRepository에서 DefaultCalendarRepository로 올바르게 변경되었습니다.

클로저에서 self를 강하게 캡처하는 메모리 누수 문제는 이전 리뷰에서 이미 언급되었습니다.

Cherrish-iOS/Cherrish-iOS/Data/Model/Calendar/CalendarDowntimeResponseDTO.swift (1)

10-18: DTO 구조가 적절합니다.

scheduledAt 필드가 toEntity() 매핑에서 사용되지 않는 문제는 이전 리뷰에서 이미 언급되었습니다.

Cherrish-iOS/Cherrish-iOS/Data/Model/Calendar/CalendarMonthlyResponseDTO.swift (1)

14-22: 키 변환 로직이 올바르게 구현되었습니다.

String 키를 Int로 변환하여 MonthlyEntity를 생성하는 로직이 적절합니다.

숫자가 아닌 키 변환 실패 시 로깅 추가 제안은 이전 리뷰에서 이미 언급되었습니다.

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

26-26: isDDaylet으로 선언하는 것이 적절합니다.

이 제안은 이전 리뷰에서 이미 언급되었습니다.

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

10-24: 구현이 올바릅니다. 네이밍 일관성 검토를 권장합니다.

Use case 구현 로직이 정확하며 CalendarInterface와 올바르게 연동됩니다.

다만, 구현체 이름 DefaultFetchTodayProcedureUseCase에서 "List"가 누락되어 프로토콜명 FetchTodayProcedureListUseCase와 불일치합니다. 일관성을 위해 DefaultFetchTodayProcedureListUseCase로 변경을 고려하세요.

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

123-126: 시간대 고정 후 다른 계산과의 일관성 확인 필요.

이전 리뷰에서 지적된 문제입니다. KST로 고정했지만 isSelected, extractDate 등 다른 메서드들은 Calendar.current를 사용합니다. 디바이스 타임존이 다를 때 월/일 계산이 달라질 수 있습니다.


109-112: 실패 시 상태 불일치 가능—업데이트 순서 조정 필요.

이전 리뷰에서 지적된 문제가 아직 수정되지 않았습니다. fetch 실패 시 treatmentDate만 갱신되고 procedureList는 이전 값으로 남습니다.

🔧 수정 제안
     `@MainActor`
     func fetchTodayProcedureList() async throws {
-        treatmentDate = selectedDate.toDateString()
-        procedureList = try await fetchTodayProcedureListUseCase.execute(date: treatmentDate)
+        let dateString = selectedDate.toDateString()
+        let list = try await fetchTodayProcedureListUseCase.execute(date: dateString)
+        treatmentDate = dateString
+        procedureList = list
     }
Cherrish-iOS/Cherrish-iOS/Data/Repository/CalendarRepository.swift (2)

14-19: 주입된 의존성이 무시되고 있습니다.

이전 리뷰에서 지적된 문제가 아직 수정되지 않았습니다. 초기화 매개변수를 받지만 항상 기본 구현체로 덮어쓰고 있어 DI가 무력화됩니다.

🔧 수정 제안
     init(
-        networkService: NetworkService,
-        userDefaultService: UserDefaultService
+        networkService: NetworkService = DefaultNetworkService(),
+        userDefaultService: UserDefaultService = DefaultUserDefaultService()
     ) {
-        self.networkService = DefaultNetworkService()
-        self.userDefaultService = DefaultUserDefaultService()
+        self.networkService = networkService
+        self.userDefaultService = userDefaultService
     }

64-90: MockCalendarRepository가 CalendarInterface 프로토콜을 만족하지 않습니다.

이전 리뷰에서 지적된 문제가 아직 수정되지 않았습니다. fetchProcedureCountOfMonthfetchTodayProcedureListasync throws가 누락되어 프로토콜 요구사항과 맞지 않아 컴파일 오류가 발생합니다.

🔧 수정 제안
-    func fetchProcedureCountOfMonth(year: Int, month: Int) -> MonthlyEntity {
+    func fetchProcedureCountOfMonth(year: Int, month: Int) async throws -> MonthlyEntity {
         return MonthlyEntity.init(
             dailyProcedureCounts: [
         )
     }
     
-    func fetchTodayProcedureList(date: String) -> [DailyProcedureEntity] {
+    func fetchTodayProcedureList(date: String) async throws -> [DailyProcedureEntity] {
Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarView.swift (3)

50-58: 월 변경 시 데이터 갱신이 필요합니다.

이전 리뷰에서 지적된 문제가 아직 수정되지 않았습니다. calendarMode == .none 가드로 인해 시술 상세 모드에서 월을 변경해도 fetchProcedureCountsOfMonth()가 실행되지 않아 달력 데이터가 갱신되지 않습니다.

🔧 수정 제안
         .task (id: viewModel.currentMonth){
-            if calendarMode == .none {
-                do {
-                    try await viewModel.fetchProcedureCountsOfMonth()
-                    try await viewModel.fetchTodayProcedureList()
-                } catch {
-                    CherrishLogger.error(error)
-                }
+            do {
+                try await viewModel.fetchProcedureCountsOfMonth()
+                try await viewModel.fetchTodayProcedureList()
+            } catch {
+                CherrishLogger.error(error)
             }
         }

113-113: selectedProcedureID ?? 0 사용 시 잠재적 문제.

이전 리뷰에서 지적된 문제입니다. 0이 유효한 procedure ID라면 의도치 않은 D-Day 표시가 될 수 있습니다. -1 같은 무효한 값을 사용하거나 Optional을 직접 전달하는 것이 안전합니다.


177-189: 연속 탭 시 race condition 가능성.

이전 리뷰에서 지적된 문제가 아직 수정되지 않았습니다. 빠르게 연속으로 다른 procedure를 탭하면 이전 요청이 완료되기 전에 새 요청이 시작되어 selectedDowntime이 현재 선택과 맞지 않는 데이터로 업데이트될 수 있습니다.

🔧 수정 제안: Task 취소 처리 추가

View에 상태 추가:

`@State` private var currentDowntimeTask: Task<Void, Never>? = nil

onTapGesture 수정:

                         .onTapGesture {
                             calendarMode.toggle()
                             selectedProcedureID = procedure.procedureId
                             
+                            currentDowntimeTask?.cancel()
-                            Task {
+                            currentDowntimeTask = Task {
                                 do {
                                     try await viewModel.fetchDowntimeByDay(procedureId: procedure.procedureId)
                                 } catch {
+                                    if Task.isCancelled { return }
                                     CherrishLogger.error(error)
                                 }
                             }
                         }

Comment on lines +24 to +30
guard let homeRepository = DIContainer.shared.resolve(type: HomeInterface.self) else {
return
}

guard let treatmentCategoryRepository = DIContainer.shared.resolve(type: TreatmentInterface.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.

⚠️ Potential issue | 🟡 Minor

사용되지 않는 dependency resolution을 제거하세요.

treatmentCategoryRepository가 Line 28-30에서 resolve되지만 이후 코드에서 전혀 사용되지 않습니다. Line 60에서 동일한 타입(TreatmentInterface)을 treatmentRepository로 다시 resolve하고 있습니다.

불필요한 guard 문을 제거하거나, 하나의 resolution으로 통합하세요.

♻️ 제안된 수정
-        guard let treatmentCategoryRepository = DIContainer.shared.resolve(type: TreatmentInterface.self) else {
-            return
-        }
-        
📝 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
guard let homeRepository = DIContainer.shared.resolve(type: HomeInterface.self) else {
return
}
guard let treatmentCategoryRepository = DIContainer.shared.resolve(type: TreatmentInterface.self) else {
return
}
guard let homeRepository = DIContainer.shared.resolve(type: HomeInterface.self) else {
return
}
🤖 Prompt for AI Agents
In `@Cherrish-iOS/Cherrish-iOS/Domain/DomainDependencyAssembler.swift` around
lines 24 - 30, Remove the redundant dependency resolution for
TreatmentInterface: delete the unused guard that assigns
treatmentCategoryRepository (the guard using DIContainer.shared.resolve(type:
TreatmentInterface.self) at the top) and rely on the existing resolution that
assigns treatmentRepository later (the resolve for TreatmentInterface in
treatmentRepository). Ensure only one DIContainer.shared.resolve(type:
TreatmentInterface.self) remains and that all uses reference treatmentRepository
so no unused variables remain.

Comment on lines 40 to 42
guard let homeRepository = DIContainer.shared.resolve(type: HomeInterface.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.

⚠️ Potential issue | 🟡 Minor

중복된 homeRepository resolution을 제거하세요.

homeRepository가 Line 24-26에서 이미 resolve되었는데, Line 40-42에서 동일한 타입으로 다시 resolve하고 있습니다. 이는 불필요한 중복이며, 첫 번째 resolution이 실패하면 함수가 이미 반환되므로 두 번째 guard는 도달할 수 없거나 불필요합니다.

♻️ 제안된 수정
         DIContainer.shared.register(type: FetchTodayProcedureListUseCase.self) {
             return DefaultFetchTodayProcedureUseCase(repository: calendarRepository)
         }
         
-        guard let homeRepository = DIContainer.shared.resolve(type: HomeInterface.self) else {
-            return
-        }
-        
         DIContainer.shared.register(type: FetchProcedureDowntimeUseCase.self) {
📝 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
guard let homeRepository = DIContainer.shared.resolve(type: HomeInterface.self) else {
return
}
🤖 Prompt for AI Agents
In `@Cherrish-iOS/Cherrish-iOS/Domain/DomainDependencyAssembler.swift` around
lines 40 - 42, Remove the duplicate resolution of HomeInterface: delete the
second guard that calls DIContainer.shared.resolve(type: HomeInterface.self) and
instead reuse the earlier homeRepository obtained on lines 24-26 (the originally
resolved variable named homeRepository) in the code paths that follow; ensure
there are no shadowed variables with the same name and that any subsequent
references use the initial homeRepository instance rather than resolving again.

return topGlobalY < initial - 0.1
}
}
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

들여쓰기 불일치.

닫는 중괄호의 들여쓰기가 2칸으로 되어 있습니다. 프로젝트의 다른 부분과 일관성을 위해 4칸으로 맞추는 것을 권장합니다.

🤖 Prompt for AI Agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarView.swift`
at line 330, The closing brace in CalendarView.swift for the CalendarView
declaration/closure is indented with 2 spaces instead of the project's 4-space
style; locate the mismatched brace in the CalendarView (e.g., end of
struct/extension or body where the lone "}" appears) and re-indent that closing
brace to 4 spaces so it matches the rest of the file and project coding style.

Comment on lines +78 to +91
func isDDay(for date: Date, selectedProcedureID: Int) -> Bool {
guard let selectedDowntime, selectedDowntime.procedureId == selectedProcedureID else { return false }
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
formatter.locale = Locale(identifier: "ko_KR")
formatter.timeZone = TimeZone(identifier: "Asia/Seoul")

guard let targetDate = formatter.date(from: selectedDowntime.recoveryTargetDate) else { return false }
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = TimeZone(identifier: "Asia/Seoul")!

let result = calendar.isDate(date, inSameDayAs: targetDate)
return result
}
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를 매번 생성하는 것은 비효율적입니다.

isDDay가 캘린더 셀마다 호출될 수 있어 DateFormatter를 반복 생성하면 성능에 영향을 줄 수 있습니다. 정적 프로퍼티나 lazy 프로퍼티로 캐싱하는 것을 권장합니다.

🔧 수정 제안
// CalendarViewModel 내부에 추가
private static let kstDateFormatter: DateFormatter = {
    let formatter = DateFormatter()
    formatter.dateFormat = "yyyy-MM-dd"
    formatter.locale = Locale(identifier: "ko_KR")
    formatter.timeZone = TimeZone(identifier: "Asia/Seoul")
    return formatter
}()

private static let kstCalendar: Calendar = {
    var calendar = Calendar(identifier: .gregorian)
    calendar.timeZone = TimeZone(identifier: "Asia/Seoul") ?? .current
    return calendar
}()

func isDDay(for date: Date, selectedProcedureID: Int) -> Bool {
    guard let selectedDowntime, selectedDowntime.procedureId == selectedProcedureID else { return false }
    guard let targetDate = Self.kstDateFormatter.date(from: selectedDowntime.recoveryTargetDate) else { return false }
    return Self.kstCalendar.isDate(date, inSameDayAs: targetDate)
}
🤖 Prompt for AI Agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarViewModel.swift`
around lines 78 - 91, isDDay currently creates a DateFormatter and Calendar on
every call which is costly; add cached static properties (e.g., private static
let kstDateFormatter: DateFormatter and private static let kstCalendar:
Calendar) inside CalendarViewModel configured with "yyyy-MM-dd",
Locale(identifier: "ko_KR") and TimeZone(identifier: "Asia/Seoul") and replace
the local formatter/calendar creation in isDDay(for:selectedProcedureID:) to use
Self.kstDateFormatter and Self.kstCalendar (avoid force-unwrapping
timeZone—provide a sensible fallback) while keeping the existing guard checks
for selectedDowntime and parsing the target date.

Comment on lines +86 to +87
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = TimeZone(identifier: "Asia/Seoul")!
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

TimeZone 강제 언래핑 개선 권장.

TimeZone(identifier:)가 nil을 반환할 가능성이 있어 강제 언래핑보다 안전한 처리를 권장합니다. "Asia/Seoul"은 유효한 식별자이므로 실제 크래시 가능성은 낮지만, 방어적 코딩 관점에서 개선이 좋습니다.

🔧 수정 제안 (isDDay 및 getCurrentMonth 모두 적용)
-        calendar.timeZone = TimeZone(identifier: "Asia/Seoul")!
+        calendar.timeZone = TimeZone(identifier: "Asia/Seoul") ?? .current
📝 Committable suggestion

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

Suggested change
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = TimeZone(identifier: "Asia/Seoul")!
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = TimeZone(identifier: "Asia/Seoul") ?? .current
🤖 Prompt for AI Agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarViewModel.swift`
around lines 86 - 87, Replace the forced unwrap of TimeZone(identifier:
"Asia/Seoul") when creating the Calendar with safe handling: obtain the timezone
using guard/if-let (e.g., guard let seoulTZ = TimeZone(identifier: "Asia/Seoul")
else { ... }) or provide a fallback (e.g., TimeZone.current) and use that value
to set calendar.timeZone; apply the same change in places referenced by isDDay
and getCurrentMonth so neither uses a forced unwrap of TimeZone(identifier:).

Comment on lines 20 to +32
func toDateString() -> String {
return Date.dateFormatter.string(from: self)
}
}

extension Date {
static let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
formatter.locale = Locale(identifier: "ko_KR")
formatter.timeZone = TimeZone(secondsFromGMT: 0)

return formatter.string(from: self)
}
formatter.timeZone = TimeZone(identifier: "Asia/Seoul")
return formatter
}()
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, find the file and verify it exists
find . -name "Date+.swift" -o -name "Date+Extension.swift" | head -20

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 139


🏁 Script executed:

# Search for toDateString() call sites across the codebase
rg -n --type=swift "toDateString\s*\(" -B2 -A2 | head -100

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 1874


🏁 Script executed:

# Also check imports and usage context
rg -n --type=swift "Date\." | grep -i format | head -20

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 400


🏁 Script executed:

# Check where getDowntimeState is called
rg -n --type=swift "getDowntimeState" -B3 -A3

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 2067


🏁 Script executed:

# Get more context around line 74 in CalendarViewModel
sed -n '70,80p' "Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarViewModel.swift"

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 515


🏁 Script executed:

# Check the full context of fetchTodayProcedureList and getDowntimeState methods
sed -n '72,115p' "Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarViewModel.swift"

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 1837


🏁 Script executed:

# Check CalendarView context around line 112
sed -n '100,120p' "Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarView.swift"

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 1013


🏁 Script executed:

# Check if CalendarView or its parent has MainActor annotation
rg -n --type=swift "@MainActor" "Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarView.swift" | head -5

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 52


🏁 Script executed:

# Check CalendarViewModel for `@MainActor` class-level annotation
sed -n '1,30p' "Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarViewModel.swift"

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 1142


getDowntimeState() 메서드에 @MainActor 추가하여 스레드 안전성 명시

DateFormatter는 스레드 안전하지 않으며, 현재 코드는 메인 스레드에서만 호출되고 있습니다. 그러나 getDowntimeState()@MainActor 어노테이션이 없어 향후 실수로 백그라운드 스레드에서 호출될 경우 크래시 위험이 있습니다. fetchTodayProcedureList()처럼 명시적으로 @MainActor를 추가하여 컴파일 타임에 스레드 안전성을 보장하세요.

🔧 제안 수정안
-    func getDowntimeState(for date: Date) -> DowntimeDayState {
+    `@MainActor`
+    func getDowntimeState(for date: Date) -> DowntimeDayState {
         let key = date.toDateString()
         return downtimeByDay[key] ?? .none
     }
🤖 Prompt for AI Agents
In `@Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/Date`+.swift around
lines 20 - 32, The getDowntimeState() method must be annotated with `@MainActor`
to ensure calls that use the shared Date.dateFormatter run on the main thread;
update the declaration of getDowntimeState() to include `@MainActor` (same pattern
used by fetchTodayProcedureList()) so the compiler enforces main-thread
execution whenever getDowntimeState() accesses Date.dateFormatter or other
UI/main-only resources.

@y-eonee y-eonee merged commit ee7b0e9 into develop Jan 20, 2026
7 checks passed
@y-eonee y-eonee deleted the feat/#99-캘린더-api branch January 20, 2026 21:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Feature 새로운 기능 구현 및 API 연결 나연🐹

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] 캘린더 API 연결

4 participants