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

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,56 @@ struct CalendarView: View {
@EnvironmentObject private var calendarCoordinator: CalendarCoordinator
@StateObject var viewModel: CalendarViewModel
@StateObject var homeCalendarFlowState: HomeCalendarFlowState
@State var calendarMode: CalendarMode = .none
@State var selectedProcedureID: Int? = nil
Comment on lines 26 to 29
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

SwiftUI state 속성에 private 접근 제어자를 추가하세요.

SwiftLint 경고에 따르면, SwiftUI의 @StateObject@State 속성은 private으로 선언해야 합니다. 특히 calendarModeselectedProcedureID는 내부 상태이므로 반드시 private이어야 합니다.

♻️ 권장 수정안
 struct CalendarView: View {
     `@EnvironmentObject` private var calendarCoordinator: CalendarCoordinator
-    `@StateObject` var viewModel: CalendarViewModel
-    `@StateObject` var homeCalendarFlowState: HomeCalendarFlowState
-    `@State` var calendarMode: CalendarMode = .none
-    `@State` var selectedProcedureID: Int? = nil
+    `@StateObject` private var viewModel: CalendarViewModel
+    `@StateObject` private var homeCalendarFlowState: HomeCalendarFlowState
+    `@State` private var calendarMode: CalendarMode = .none
+    `@State` private var selectedProcedureID: Int? = nil
📝 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 viewModel: CalendarViewModel
@StateObject var homeCalendarFlowState: HomeCalendarFlowState
@State var calendarMode: CalendarMode = .none
@State var selectedProcedureID: Int? = nil
`@StateObject` private var viewModel: CalendarViewModel
`@StateObject` private var homeCalendarFlowState: HomeCalendarFlowState
`@State` private var calendarMode: CalendarMode = .none
`@State` private var selectedProcedureID: Int? = nil
🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 26-26: SwiftUI state properties should be private

(private_swiftui_state)


[Warning] 27-27: SwiftUI state properties should be private

(private_swiftui_state)


[Warning] 28-28: SwiftUI state properties should be private

(private_swiftui_state)


[Warning] 29-29: SwiftUI state properties should be private

(private_swiftui_state)

🤖 Prompt for AI Agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarView.swift`
around lines 26 - 29, The SwiftUI state properties are missing access control;
mark the `@StateObject` and `@State` properties as private to satisfy SwiftLint and
encapsulate internal state—update the declarations for viewModel,
homeCalendarFlowState, calendarMode, and selectedProcedureID in CalendarView
(e.g., change "@StateObject var viewModel" to "private `@StateObject` var
viewModel", and similarly make homeCalendarFlowState, calendarMode, and
selectedProcedureID private).


var body: some View {
ZStack {
if viewModel.isLoading {
CherrishLoadingView()
} else {
CalendarContentView(
viewModel: viewModel,
homeCalendarFlowState: homeCalendarFlowState,
calendarMode: $calendarMode,
selectedProcedureID: $selectedProcedureID
)
}
}
.task (id: viewModel.currentMonth){
if calendarMode == .none {
do {
try await viewModel.fetchProcedureCountsOfMonth()
try await viewModel.fetchTodayProcedureList()
} catch {
CherrishLogger.error(error)
}
}
Comment on lines +44 to +52
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

순차적 fetch 호출로 인한 로딩 상태 깜빡임(flicker) 가능성이 있습니다.

fetchProcedureCountsOfMonthfetchTodayProcedureList 모두 내부에서 isLoading = true로 설정 후 완료 시 false로 설정합니다. 순차 호출 시 true → false → true → false 순서로 변경되어 로딩 뷰가 깜빡일 수 있습니다.

또한, 첫 번째 호출이 실패하면 두 번째 호출이 실행되지 않아 불완전한 상태가 될 수 있습니다.

🔧 권장 수정안

ViewModel에서 로딩 상태를 별도로 관리하거나, 두 호출을 병렬로 실행하고 외부에서 로딩 상태를 제어하는 방식을 고려하세요:

 .task (id: viewModel.currentMonth){
     if calendarMode == .none {
+        viewModel.isLoading = true
         do {
-            try await viewModel.fetchProcedureCountsOfMonth()
-            try await viewModel.fetchTodayProcedureList()
+            async let counts: () = viewModel.fetchProcedureCountsOfMonthWithoutLoading()
+            async let list: () = viewModel.fetchTodayProcedureListWithoutLoading()
+            _ = try await (counts, list)
         } catch {
             CherrishLogger.error(error)
         }
+        viewModel.isLoading = false
     }
 }

또는 ViewModel에 두 작업을 래핑하는 단일 메서드를 추가하세요.

🤖 Prompt for AI Agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarView.swift`
around lines 44 - 52, The two sequential calls
viewModel.fetchProcedureCountsOfMonth and viewModel.fetchTodayProcedureList
cause isLoading toggles and potential flicker and also prevent the second call
if the first fails; fix by either (A) adding a ViewModel wrapper like
fetchMonthData() that sets isLoading = true once, starts both operations (e.g.,
using async let or TaskGroup inside the wrapper), awaits both, aggregates
errors, then sets isLoading = false, and call that from the .task, or (B) change
the .task body to run both in parallel (use async let for
viewModel.fetchProcedureCountsOfMonth and viewModel.fetchTodayProcedureList) and
handle errors so both run and you control a single loading flag (ensure you
reference viewModel.isLoading or the new wrapper method names to locate the
changes).

}
.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 struct CalendarContentView: View {
@EnvironmentObject private var calendarCoordinator: CalendarCoordinator
@ObservedObject var viewModel: CalendarViewModel
@ObservedObject var homeCalendarFlowState: HomeCalendarFlowState
@State private var topGlobalY: CGFloat = .zero
@State private var initialTopGlobalY: CGFloat? = nil
@State private var bottomOffsetY: CGFloat = .zero
@State private var calendarMode: CalendarMode = .none
@State private var selectedProcedureID: Int? = nil
@Binding var calendarMode: CalendarMode
@Binding var selectedProcedureID: Int?
@State private var buttonState: ButtonState = .active

private let scrollAreaHeight: CGFloat = 184.adjustedH
Expand All @@ -40,6 +85,7 @@ struct CalendarView: View {
let weekdays: [String] = ["일", "월", "화", "수", "목", "금", "토"]
let columns = Array(repeating: GridItem(.fixed(40.adjustedW), spacing: 8), count: 7)


var body: some View {
VStack {
Spacer()
Expand All @@ -56,33 +102,10 @@ struct CalendarView: View {
}
Spacer()
}
.task (id: viewModel.currentMonth){
if calendarMode == .none {
do {
try await viewModel.fetchProcedureCountsOfMonth()
try await viewModel.fetchTodayProcedureList()
} catch {
CherrishLogger.error(error)
}
}
}
.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
}
}
.background(.gray0)
}
}

extension CalendarView {
extension CalendarContentView {
private var calendarHeader: some View {
VStack {
HStack {
Expand Down Expand Up @@ -330,7 +353,7 @@ extension CalendarView {

}

extension CalendarView {
extension CalendarContentView {
private var scrollViewTopMarkerView: some View {
GeometryReader { proxy in
Color.clear
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ final class CalendarViewModel: ObservableObject {
@Published private(set) var treatmentDate: String = ""
@Published private(set) var downtimeByDay: [String : DowntimeDayState] = [:]
@Published private(set) var selectedDowntime: ProcedureDowntimeEntity?
@Published private(set) var isLoading: Bool = true

private let fetchProcedureCountOfMonthUseCase: FetchProcedureCountOfMonth
private let fetchTodayProcedureListUseCase: FetchTodayProcedureListUseCase
Expand Down Expand Up @@ -103,26 +104,46 @@ final class CalendarViewModel: ObservableObject {

@MainActor
func fetchProcedureCountsOfMonth() async throws {
isLoading = true
let calendar = Calendar.current
let targetDate = getCurrentMonth(addingMonth: currentMonth)
let year = calendar.component(.year, from: targetDate)
let month = calendar.component(.month, from: targetDate)

let response = try await fetchProcedureCountOfMonthUseCase.execute(year: year, month: month)
procedureCountOfMonth = response.dailyProcedureCounts
do {
let response = try await fetchProcedureCountOfMonthUseCase.execute(year: year, month: month)
isLoading = false
procedureCountOfMonth = response.dailyProcedureCounts
} catch {
CherrishLogger.error(error)
}
Comment on lines +113 to +119
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

에러 발생 시 isLoadingfalse로 설정되지 않아 무한 로딩 상태가 됩니다.

catch 블록에서 isLoading = false가 누락되어 있습니다. 네트워크 오류 발생 시 사용자는 영원히 로딩 화면을 보게 됩니다. HomeViewModel.loadDashboard()의 패턴처럼 isLoading = falsedo-catch 이후 또는 catch 블록 내에서 호출해야 합니다.

🐛 수정 제안
         do {
             let response = try await fetchProcedureCountOfMonthUseCase.execute(year: year, month: month)
-            isLoading = false
             procedureCountOfMonth = response.dailyProcedureCounts
         } catch {
             CherrishLogger.error(error)
         }
+        isLoading = false
🤖 Prompt for AI Agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarViewModel.swift`
around lines 113 - 119, The do-catch in CalendarViewModel around
fetchProcedureCountOfMonthUseCase.execute currently sets isLoading = false only
on success, causing infinite loading on errors; update the function so isLoading
is always set to false after the await (either add isLoading = false in the
catch block or use a defer/finally-style pattern) and keep assigning
procedureCountOfMonth from response only on success—refer to CalendarViewModel
and the fetchProcedureCountOfMonthUseCase.execute call to locate the code to
change.

Copy link
Contributor

Choose a reason for hiding this comment

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

이고 한 번 화긴해주세용!

}

@MainActor
func fetchTodayProcedureList() async throws {
isLoading = true
treatmentDate = selectedDate.toDateString()
procedureList = try await fetchTodayProcedureListUseCase.execute(date: treatmentDate)

do {
procedureList = try await fetchTodayProcedureListUseCase.execute(date: treatmentDate)
isLoading = false
} catch {
CherrishLogger.error(error)
}
Comment on lines +127 to +132
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

동일한 이슈: 에러 시 isLoading이 초기화되지 않습니다.

🐛 수정 제안
         do {
             procedureList = try await fetchTodayProcedureListUseCase.execute(date: treatmentDate)
-            isLoading = false
         } catch {
             CherrishLogger.error(error)
         }
+        isLoading = false
📝 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
do {
procedureList = try await fetchTodayProcedureListUseCase.execute(date: treatmentDate)
isLoading = false
} catch {
CherrishLogger.error(error)
}
do {
procedureList = try await fetchTodayProcedureListUseCase.execute(date: treatmentDate)
} catch {
CherrishLogger.error(error)
}
isLoading = false
🤖 Prompt for AI Agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarViewModel.swift`
around lines 127 - 132, The catch block currently logs errors but fails to reset
isLoading, causing the UI to remain in a loading state; update the error
handling for fetchTodayProcedureListUseCase.execute (used to assign
procedureList) so that isLoading is set to false in all paths — either by
setting isLoading = false inside the catch block alongside
CherrishLogger.error(error) or by restructuring the async call with a
defer/finally-like pattern to guarantee isLoading is cleared after the await
completes or errors.

}

@MainActor
func fetchDowntimeByDay(procedureId: Int) async throws {
let downtimeList = try await fetchProcedureDowntimeUseCase.execute(id: procedureId)
selectedDowntime = downtimeList
mapToDowntimeDays(procedure: downtimeList)
isLoading = true

do {
let downtimeList = try await fetchProcedureDowntimeUseCase.execute(id: procedureId)
isLoading = false
selectedDowntime = downtimeList
mapToDowntimeDays(procedure: downtimeList)
} catch {
CherrishLogger.error(error)
}
Comment on lines +139 to +146
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

동일한 이슈: 에러 시 isLoading이 초기화되지 않습니다.

🐛 수정 제안
         do {
             let downtimeList = try await fetchProcedureDowntimeUseCase.execute(id: procedureId)
-            isLoading = false 
             selectedDowntime = downtimeList
             mapToDowntimeDays(procedure: downtimeList)
         } catch {
             CherrishLogger.error(error)
         }
+        isLoading = false
📝 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
do {
let downtimeList = try await fetchProcedureDowntimeUseCase.execute(id: procedureId)
isLoading = false
selectedDowntime = downtimeList
mapToDowntimeDays(procedure: downtimeList)
} catch {
CherrishLogger.error(error)
}
do {
let downtimeList = try await fetchProcedureDowntimeUseCase.execute(id: procedureId)
selectedDowntime = downtimeList
mapToDowntimeDays(procedure: downtimeList)
} catch {
CherrishLogger.error(error)
}
isLoading = false
🤖 Prompt for AI Agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/CalendarMain/CalendarViewModel.swift`
around lines 139 - 146, The catch path for the
fetchProcedureDowntimeUseCase.execute call doesn't reset isLoading, causing the
view to remain stuck loading; update the error handling so isLoading is set to
false on failure (either add isLoading = false inside the catch before
CherrishLogger.error(error) or refactor the surrounding logic to use a defer
that sets isLoading = false after starting the async work), keeping the rest of
the flow (selectedDowntime, mapToDowntimeDays, CherrishLogger.error) unchanged.

}

func updateDate(date: Date) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,35 +25,36 @@ struct NoTreatmentFilterView: View {
var body: some View {
VStack(spacing: 0) {
TitleHeaderView(title: viewModel.selectedCategory?.title ?? "")

ScrollView(.vertical, showsIndicators: false){
Spacer()
.frame(height: 18.adjustedH)

ForEach(viewModel.treatments, id: \.id) { treatment in
TreatmentRowView(
displayMode: .checkBoxView,
treatmentEntity: treatment,
isSelected: .constant(viewModel.isSelected(treatment)),
action: {
if viewModel.isSelected(treatment) {
viewModel.removeTreatment(treatment)
} else {
viewModel.addTreatment(treatment)

}
}
)
.padding(.horizontal, 25.adjustedW)
if viewModel.isLoading {
CherrishLoadingView()
} else {
ScrollView(.vertical, showsIndicators: false) {
Spacer()
.frame(height: 18.adjustedH)

ForEach(viewModel.treatments, id: \.id) { treatment in
TreatmentRowView(
displayMode: .checkBoxView,
treatmentEntity: treatment,
isSelected: .constant(viewModel.isSelected(treatment)),
action: {
if viewModel.isSelected(treatment) {
viewModel.removeTreatment(treatment)
} else {
viewModel.addTreatment(treatment)

}
}
)
.padding(.horizontal, 25.adjustedW)

}
Spacer()
.frame(height: 198.adjustedH)
}
Spacer()
.frame(
height: viewModel.selectedTreatments.isEmpty ?
24.adjustedH : scrollViewHeight.adjustedH + 24.adjustedH
)
}


}
.task {
await viewModel.fetchNoTreatments()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,63 +151,67 @@ private struct TreatmentSelectedCategory: View {
]

var body: some View {
VStack(spacing: 0) {
Spacer()
.frame(height: 50.adjustedH)

HStack(spacing: 0) {
VStack(alignment: .leading, spacing: 0) {
TypographyText(
"요즘 가장 신경 쓰이는 ",
style: .title1_sb_18,
color: .gray1000
)
.frame(height: 27.adjustedH)
if viewModel.isLoading {
CherrishLoadingView()
} else {
VStack(spacing: 0) {
Spacer()
.frame(height: 50.adjustedH)

HStack(spacing: 0) {
VStack(alignment: .leading, spacing: 0) {
TypographyText(
"요즘 가장 신경 쓰이는 ",
style: .title1_sb_18,
color: .gray1000
)
.frame(height: 27.adjustedH)

TypographyText(
"외모 고민은 무엇인가요?",
style: .title1_sb_18,
color: .gray1000
)
.frame(height: 27.adjustedH)
Spacer()
.frame(height: 4.adjustedH)
TypographyText(
"선택한 고민을 기준으로 시술 정보를 정리해줘요.",
style: .body1_m_14,
color: .gray700
)
.frame(height: 20.adjustedH)

}
.frame(height: 78.adjustedH)

TypographyText(
"외모 고민은 무엇인가요?",
style: .title1_sb_18,
color: .gray1000
)
.frame(height: 27.adjustedH)
Spacer()
.frame(height: 4.adjustedH)
TypographyText(
"선택한 고민을 기준으로 시술 정보를 정리해줘요.",
style: .body1_m_14,
color: .gray700
)
.frame(height: 20.adjustedH)

}
.frame(height: 78.adjustedH)
.padding(.horizontal, 34.adjustedW)

Spacer()
}
.padding(.horizontal, 34.adjustedW)

ScrollView(.vertical, showsIndicators:false) {
Spacer()
.frame(height: 40.adjustedH)
LazyVGrid(columns: columns, spacing: 12.adjustedH) {
ForEach(viewModel.categories, id: \.id) { category in
SelectionChip(
title: category.title,
isSelected: Binding(
get: {
viewModel.selectedCategory == category
},
set: {
isSelected in
guard isSelected else {
return
ScrollView(.vertical, showsIndicators:false) {
Spacer()
.frame(height: 40.adjustedH)
LazyVGrid(columns: columns, spacing: 12.adjustedH) {
ForEach(viewModel.categories, id: \.id) { category in
SelectionChip(
title: category.title,
isSelected: Binding(
get: {
viewModel.selectedCategory == category
},
set: {
isSelected in
guard isSelected else {
return
}
viewModel.selectCategory(category)
}
viewModel.selectCategory(category)
}
)
)
)
}
} .padding(.horizontal, 34.adjustedW)
}
} .padding(.horizontal, 34.adjustedW)
}
}
}
}
Expand Down
Loading