Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,7 @@ struct CalendarCellView: View {
.padding(.top, 36.adjustedH)
}

TypographyText(
"\(value.day)",
style: .body1_r_14,
color: calendarMode == .selectedProcedure && isDDay ? .red700 : .gray1000
)
TypographyText("\(value.day)", style: .body1_r_14, color: dayTextColor)

if procedureCount > 0 && calendarMode == .none {
let displayCount = min(procedureCount, 3)
Expand All @@ -74,6 +70,16 @@ struct CalendarCellView: View {
}
}
extension CalendarCellView {
private var dayTextColor: Color {
if calendarMode == .selectedProcedure && downtimeState != .none {
return .gray1000
}
if calendarMode == .selectedProcedure && isDDay {
return .red700
}
return .gray800
}

private var scheduleCircle: some View {
Circle()
.fill(.red700)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ enum CalendarMode {
struct CalendarView: View {
@EnvironmentObject private var calendarCoordinator: CalendarCoordinator
@StateObject var viewModel: CalendarViewModel
@StateObject var homeCalendarFlowState: HomeCalendarFlowState
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

@StateObject 대신 @ObservedObject를 사용해야 합니다.

homeCalendarFlowState는 DIContainer에서 외부 생성되어 주입되는 객체입니다. @StateObject는 View가 객체의 생명주기를 소유할 때 사용하고, 외부에서 생성된 객체를 전달받을 때는 @ObservedObject를 사용해야 합니다.

현재 구현에서는 싱글톤으로 등록되어 있어 실질적으로 문제가 발생하지 않을 수 있지만, 의미론적으로 올바른 property wrapper를 사용하는 것이 좋습니다.

🔧 수정 제안
-    `@StateObject` var homeCalendarFlowState: HomeCalendarFlowState
+    `@ObservedObject` var homeCalendarFlowState: HomeCalendarFlowState
📝 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
@StateObject var homeCalendarFlowState: HomeCalendarFlowState
`@ObservedObject` var homeCalendarFlowState: HomeCalendarFlowState
🤖 Prompt for AI Agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarView.swift`
at line 27, The property wrapper on homeCalendarFlowState is incorrect for an
externally injected object; replace the `@StateObject` declaration of
homeCalendarFlowState with `@ObservedObject` in CalendarView so the view does not
claim lifecycle ownership (homeCalendarFlowState is provided via
DIContainer/singleton). Update the CalendarView property declaration that
currently reads "@StateObject var homeCalendarFlowState: HomeCalendarFlowState"
to use "@ObservedObject" so the semantics match external injection.

@State private var topGlobalY: CGFloat = .zero
@State private var initialTopGlobalY: CGFloat? = nil
@State private var bottomOffsetY: CGFloat = .zero
Expand Down Expand Up @@ -65,6 +66,18 @@ struct CalendarView: View {
}
}
}
.onChange(of: homeCalendarFlowState.treatmentDate) { date in
if let date = date {
viewModel.updateDate(date: date)
homeCalendarFlowState.treatmentDate = nil
}
}
.onAppear {
if let date = homeCalendarFlowState.treatmentDate {
viewModel.updateDate(date: date)
homeCalendarFlowState.treatmentDate = nil
}
}
Comment on lines +69 to +80
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

중복 로직을 헬퍼 함수로 추출하는 것을 고려해보세요.

onAppearonChange 모두 동일한 로직(updateDate 호출 후 treatmentDate = nil 설정)을 수행합니다. 중복을 줄이기 위해 헬퍼 함수로 추출할 수 있습니다.

♻️ 제안하는 리팩토링
+    private func handleTreatmentDate(_ date: Date?) {
+        guard let date = date else { return }
+        viewModel.updateDate(date: date)
+        homeCalendarFlowState.treatmentDate = nil
+    }
+
     // In body:
     .onChange(of: homeCalendarFlowState.treatmentDate) { date in
-        if let date = date {
-            viewModel.updateDate(date: date)
-            homeCalendarFlowState.treatmentDate = nil
-        }
+        handleTreatmentDate(date)
     }
     .onAppear {
-        if let date = homeCalendarFlowState.treatmentDate {
-            viewModel.updateDate(date: date)
-            homeCalendarFlowState.treatmentDate = nil
-        }
+        handleTreatmentDate(homeCalendarFlowState.treatmentDate)
     }
📝 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
.onChange(of: homeCalendarFlowState.treatmentDate) { date in
if let date = date {
viewModel.updateDate(date: date)
homeCalendarFlowState.treatmentDate = nil
}
}
.onAppear {
if let date = homeCalendarFlowState.treatmentDate {
viewModel.updateDate(date: date)
homeCalendarFlowState.treatmentDate = nil
}
}
private func handleTreatmentDate(_ date: Date?) {
guard let date = date else { return }
viewModel.updateDate(date: date)
homeCalendarFlowState.treatmentDate = nil
}
// ... (other code in body)
.onChange(of: homeCalendarFlowState.treatmentDate) { date in
handleTreatmentDate(date)
}
.onAppear {
handleTreatmentDate(homeCalendarFlowState.treatmentDate)
}
🤖 Prompt for AI Agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarView.swift`
around lines 69 - 80, Extract the duplicated logic that checks
homeCalendarFlowState.treatmentDate, calls viewModel.updateDate(date:), and sets
homeCalendarFlowState.treatmentDate = nil into a single helper method (e.g.,
handleTreatmentDate()) inside CalendarView; then replace the bodies of both the
.onChange(of: homeCalendarFlowState.treatmentDate) closure and the .onAppear
closure to call that helper, ensuring you reference the same
viewModel.updateDate(date:) and mutate homeCalendarFlowState.treatmentDate from
within the helper so behavior remains identical.

.background(.gray0)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,29 @@ final class CalendarViewModel: ObservableObject {
selectedDowntime = downtimeList
mapToDowntimeDays(procedure: downtimeList)
}

func updateDate(date: Date) {
selectedDate = date

let calendar = Calendar.current
let currentYear = calendar.component(.year, from: currentDate)
let currentMonthVal = calendar.component(.month, from: currentDate)

let targetYear = calendar.component(.year, from: date)
let targetMonthVal = calendar.component(.month, from: date)

let monthDiff = (targetYear - currentYear) * 12 + (targetMonthVal - currentMonthVal)
currentMonth = monthDiff

Task {
do {
try await fetchProcedureCountsOfMonth()
try await fetchTodayProcedureList()
} catch {
CherrishLogger.error(error)
}
}
Comment on lines +141 to +148
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

비동기 작업의 취소 처리가 누락되었습니다.

updateDate가 빠르게 연속 호출되면 여러 Task가 동시에 실행되어 이전 요청의 결과가 최신 상태를 덮어쓸 수 있습니다. 또한, view가 사라질 때 Task가 취소되지 않습니다.

♻️ Task 관리 개선 제안

ViewModel에 Task 참조를 저장하고 새 호출 시 이전 Task를 취소하는 방식을 고려해보세요:

+    private var updateTask: Task<Void, Never>?
+
     func updateDate(date: Date) {
         selectedDate = date
         
         let calendar = Calendar.current
         let currentYear = calendar.component(.year, from: currentDate)
         let currentMonthVal = calendar.component(.month, from: currentDate)
         
         let targetYear = calendar.component(.year, from: date)
         let targetMonthVal = calendar.component(.month, from: date)
         
         let monthDiff = (targetYear - currentYear) * 12 + (targetMonthVal - currentMonthVal)
         currentMonth = monthDiff
         
-        Task {
+        updateTask?.cancel()
+        updateTask = Task {
             do {
                 try await fetchProcedureCountsOfMonth()
                 try await fetchTodayProcedureList()
             } catch {
                 CherrishLogger.error(error)
             }
         }
     }
🤖 Prompt for AI Agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarViewModel.swift`
around lines 141 - 148, The code launches detached Tasks in CalendarViewModel
without cancellation, so rapid updateDate calls or view disappearance can spawn
overlapping tasks and stale results; add a stored Task? property (e.g.
fetchTask) on CalendarViewModel, cancel the existing fetchTask before creating a
new Task in updateDate, assign the new Task to fetchTask, and ensure you cancel
fetchTask in deinit or when the view disappears; also handle CancellationError
inside the Task (or check Task.isCancelled) so fetchProcedureCountsOfMonth and
fetchTodayProcedureList results are not applied after cancellation.

}
}

extension CalendarViewModel {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,11 @@ final class TreatmentViewModel: ObservableObject{
}

do {
try await createUserProcedureUseCase.execute(scheduledDate: scheduledDate.toScheduledAtFormat, recoveryDate: recoverDate.toRecoveryDateFormat, treatments: selectedTreatments)
try await createUserProcedureUseCase.execute(
scheduledDate: scheduledDate.toScheduledAtFormat,
recoveryDate: recoverDate.toRecoveryDateFormat,
treatments: selectedTreatments
)
Comment on lines +81 to +85
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

코드 포맷팅 개선 확인

함수 호출을 여러 줄로 분리하여 가독성을 향상시켰습니다. 파라미터 순서와 값은 변경되지 않았으며, 로직에 영향을 주지 않는 포맷팅 변경입니다.

다만, 이 변경사항은 PR의 주요 목적(캘린더 선택 날짜 네비게이션)과 직접적인 관련이 없는 것으로 보입니다. 포맷팅 개선은 좋지만, 별도의 커밋이나 PR로 분리하는 것이 변경 이력 관리에 더 명확할 수 있습니다.

🤖 Prompt for AI Agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/Treatment/ViewModel/Treatment/TreatmentViewModel.swift`
around lines 81 - 85, 포맷팅 변경이 기능과 직접 관련되지 않으므로, TreatmentViewModel.swift의
createUserProcedureUseCase.execute(...) 호출부에서 여러 줄로 나눈 포맷팅 변경을 별도의 커밋(또는 PR)으로
분리하세요: 현재 변경된 createUserProcedureUseCase.execute 호출을 원래 기능 관련 커밋에서 제거하고 포맷팅 전/후
중 원하는 스타일을 반영한 작은 커밋을 새로 만들어 푸시한 뒤 이 PR은 캘린더 네비게이션 관련 변경만 포함하도록 정리하십시오.

} catch {
CherrishLogger.network(error)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//
// HomeCalendarFlowState.swift
// Cherrish-iOS
//
// Created by 이나연 on 1/21/26.
//

import Foundation

final class HomeCalendarFlowState: ObservableObject {
@Published var treatmentDate: Date?
}
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,6 @@ private struct UpcomingBoxView: View {
)
.cherrishShadow()
.padding(.horizontal, 24.adjustedW)

}

private var upcomingListView: some View {
Expand All @@ -337,6 +336,10 @@ private struct UpcomingBoxView: View {
dDay: item.dDay
)
}
.onTapGesture {
viewModel.selectUpcomingDate(item.date)
tabBarCoordinator.switchTab(tab: .calendar)
}
}
}
.padding(.top, 11.adjustedH)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,14 @@ final class HomeViewModel: ObservableObject {
@Published var errorMessage: String?

private let fetchDashboardDataUseCase: FetchDashboardData
private let homeCalendarFlowState: HomeCalendarFlowState

init(fetchDashboardDataUseCase: FetchDashboardData) {
init(
fetchDashboardDataUseCase: FetchDashboardData,
homeCalendarFlowState: HomeCalendarFlowState
) {
self.fetchDashboardDataUseCase = fetchDashboardDataUseCase
self.homeCalendarFlowState = homeCalendarFlowState
}

@MainActor
Expand All @@ -33,6 +38,14 @@ final class HomeViewModel: ObservableObject {
isLoading = false
}

func selectUpcomingDate(_ dateString: String) {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
if let date = formatter.date(from: dateString) {
homeCalendarFlowState.treatmentDate = date
}
Comment on lines +41 to +46
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

🌐 Web query:

Apple DateFormatter fixed format parsing locale calendar en_US_POSIX best practices

💡 Result:

Short best-practice checklist for fixed-format parsing with DateFormatter (Swift / Foundation)

  • Use a POSIX locale: formatter.locale = Locale(identifier: "en_US_POSIX") — this prevents user settings (24‑hour, language, etc.) from changing your format. (developer.apple.com)
  • Set an explicit calendar (usually Gregorian) when you need calendar-stable parsing: formatter.calendar = Calendar(identifier: .gregorian). Some locales use non‑Gregorian calendars which can change numeric interpretation. (stackoverflow.com)
  • Set an explicit timeZone for parsing (often UTC for internet timestamps): formatter.timeZone = TimeZone(secondsFromGMT: 0). (developer.apple.com)
  • Prefer specialized APIs when available: use ISO8601DateFormatter for ISO 8601 / RFC 3339 strings (less error-prone and faster). (sarunw.com)
  • For user-visible output, avoid fixed dateFormat; use dateStyle/timeStyle or localized templates (DateFormatter.dateFormatFromTemplate or setLocalizedDateFormatFromTemplate) so the UI respects user preferences. (developer.apple.com)
  • Cache formatters but handle locale changes: either create POSIX parsers on demand (they’re locale-invariant) or observe NSCurrentLocaleDidChangeNotification and rebuild cached formatters when user locale changes. (developer.apple.com)
  • When strict fixed-format parsing is critical, consider C functions (strptime_l/strftime_l) with the POSIX/C locale for deterministic behavior. (developer.apple.com)

Minimal example (Swift):

let f = DateFormatter()
f.locale = Locale(identifier: "en_US_POSIX")
f.calendar = Calendar(identifier: .gregorian)
f.timeZone = TimeZone(secondsFromGMT: 0)
f.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
let date = f.date(from: isoString) // deterministic parsing

(Or use ISO8601DateFormatter for RFC 3339/ISO 8601 inputs.) (sarunw.com)

References (Apple):

Citations:


고정 포맷 파싱 시 Locale/Calendar 명시 필요.

현재 코드에서 명시적인 로케일과 캘린더가 없으므로 "yyyy-MM-dd" 파싱이 사용자의 기기 설정에 따라 실패할 수 있습니다. Apple의 공식 가이드에 따르면 고정 포맷 파싱에는 en_US_POSIX 로케일과 그레고리력을 명시해야 하며, 백엔드 기준 타임존이 있다면 timeZone도 지정해주세요.

🔧 제안 수정
-        let formatter = DateFormatter()
-        formatter.dateFormat = "yyyy-MM-dd"
+        let formatter = DateFormatter()
+        formatter.locale = Locale(identifier: "en_US_POSIX")
+        formatter.calendar = Calendar(identifier: .gregorian)
+        formatter.dateFormat = "yyyy-MM-dd"
🤖 Prompt for AI Agents
In `@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Home/HomeViewModel.swift`
around lines 41 - 46, The date parsing in selectUpcomingDate(_:) uses a fixed
format but doesn't set the DateFormatter.locale, calendar, or timeZone, which
can cause failures on some devices; update the DateFormatter in
selectUpcomingDate to set formatter.locale = Locale(identifier: "en_US_POSIX"),
formatter.calendar = Calendar(identifier: .gregorian) and (if backend expects a
specific zone) set formatter.timeZone appropriately before calling
formatter.date(from:), then assign the parsed Date to
homeCalendarFlowState.treatmentDate.

}

var formattedDate: String {
guard let date = dashboardData?.date else { return "" }
let formatter = DateFormatter()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,14 @@ final class PresentationDependencyAssembler: DependencyAssembler {
func assemble() {
preAssembler.assemble()

let calendarTreatmentFlowState = CalendarTreatmentFlowState()
DIContainer.shared.register(type: CalendarTreatmentFlowState.self) {
return CalendarTreatmentFlowState()
return calendarTreatmentFlowState
}

let homeCalendarFlowState = HomeCalendarFlowState()
DIContainer.shared.register(type: HomeCalendarFlowState.self) {
return homeCalendarFlowState
}

guard let createProfileUseCase = DIContainer.shared.resolve(type: CreateProfileUseCase.self) else {
Expand All @@ -38,11 +44,6 @@ final class PresentationDependencyAssembler: DependencyAssembler {
return
}

guard let calendarTreatmentFlowState = DIContainer.shared.resolve(type: CalendarTreatmentFlowState.self) else {
CherrishLogger.error(CherrishError.DIFailedError)
return
}

DIContainer.shared.register(type: CalendarViewModel.self) {
return CalendarViewModel(
fetchProcedureCountOfMonthUseCase: fetchProcedureCountOfMonthUseCase,
Expand All @@ -57,7 +58,10 @@ final class PresentationDependencyAssembler: DependencyAssembler {
}

DIContainer.shared.register(type: HomeViewModel.self) {
return HomeViewModel(fetchDashboardDataUseCase: fetchDashboardData)
return HomeViewModel(
fetchDashboardDataUseCase: fetchDashboardData,
homeCalendarFlowState: homeCalendarFlowState
)
}

guard let fetchTreatmentCategoriesUseCase = DIContainer.shared.resolve(type: FetchTreatmentCategoriesUseCase.self) else {
Expand Down
5 changes: 3 additions & 2 deletions Cherrish-iOS/Cherrish-iOS/Presentation/ViewFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,11 @@ final class ViewFactory: ViewFactoryProtocol {
}

func makeCalendarView() -> CalendarView {
guard let viewModel = DIContainer.shared.resolve(type: CalendarViewModel.self) else {
guard let viewModel = DIContainer.shared.resolve(type: CalendarViewModel.self),
let homeCalendarFlowState = DIContainer.shared.resolve(type: HomeCalendarFlowState.self) else {
fatalError()
}
return CalendarView(viewModel: viewModel)
return CalendarView(viewModel: viewModel, homeCalendarFlowState: homeCalendarFlowState)
}

func makeMyPageView() -> MyPageView {
Expand Down