Conversation
📝 WalkthroughWalkthrough시술(Treatment) 선택 플로우를 구현합니다. 새로운 상태 관리 뷰모델, 검색 및 필터링 기능, 시술 선택 UI 컴포넌트를 추가하고, 기존 SelectedTreatmentView를 프롭 기반으로 리팩토링합니다. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant TreatmentView
participant TreatmentViewModel
participant TreatmentFilterView
participant SelectedTreatmentView
User->>TreatmentView: View loads (treatmentFilter state)
TreatmentView->>TreatmentViewModel: Observe state & selectedTreatments
TreatmentView->>TreatmentFilterView: Render with ViewModel
User->>TreatmentFilterView: Search for treatment
TreatmentFilterView->>TreatmentViewModel: Update searchText
TreatmentViewModel->>TreatmentViewModel: Debounce (300ms) & filter
TreatmentViewModel->>TreatmentFilterView: Publish filteredTreatments
TreatmentFilterView->>User: Display filtered list
User->>TreatmentFilterView: Tap treatment row
TreatmentFilterView->>TreatmentViewModel: addTreatment(treatment)
TreatmentViewModel->>TreatmentViewModel: Append to selectedTreatments
TreatmentView->>SelectedTreatmentView: Render (if selectedTreatments.count > 0)
SelectedTreatmentView->>User: Display selected treatments
User->>SelectedTreatmentView: Tap remove button
SelectedTreatmentView->>TreatmentViewModel: removeTreatment(treatment)
TreatmentViewModel->>TreatmentViewModel: Remove from selectedTreatments
User->>TreatmentView: Tap next button
TreatmentView->>TreatmentViewModel: next()
TreatmentViewModel->>TreatmentViewModel: Advance state
TreatmentView->>User: Navigate to next step
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
| } | ||
|
|
||
| extension TreatmentEntity { | ||
| static let mockData: [TreatmentEntity] = [ |
|
|
||
| import SwiftUI | ||
|
|
||
| struct SelectedTreatmentView: View { |
There was a problem hiding this comment.
뒤에 sheet 붙이는 네이밍으로 바꾸는건 어떨까요
| x: 0, | ||
| y: -5 | ||
| ) | ||
| ScrollView(.vertical, showsIndicators: false) { |
| VStack(alignment: .leading, spacing: 0) { | ||
| TypographyText("회복을 계획할 때 고려해야 할", style: .title1_sb_18, color: .gray1000) | ||
| TypographyText("중요한 일정이 있나요?", style: .title1_sb_18, color: .gray1000) | ||
| } |
| VStack { | ||
|
|
| .padding(.vertical, 8.adjustedH) | ||
| .padding(.leading, 16.5.adjustedW) | ||
| Button{ |
| } | ||
| ScrollView(.vertical, showsIndicators: false) { | ||
| ForEach(treatments, id: \.self) { treatment in | ||
| TreatmentRowView(displayMode: .completeBoxView, treatmentEntity: treatment, isSelected: .constant(false), isCompleted: .constant(false), action: {}) |
There was a problem hiding this comment.
이 플로우까지 gif에 첨부되지 않은것 같은데 추가해주세요!
| var isLast: Bool { self == Self.allCases.last } | ||
| } | ||
|
|
||
| // MARK: - Navigation |
| @ObservedObject var viewModel: TreatmentViewModel | ||
| var body: some View { | ||
| VStack { | ||
| TreatmentSearchBarTextField(text: $viewModel.searchText, enter: { }, isDisabled: !viewModel.searchText.isEmpty) |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Fix all issues with AI agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/Treatment/Components/TreatmentSearchBarTextField.swift`:
- Around line 26-33: The TextField's onSubmit currently fires even when searches
are meant to be disabled; update the component to both disable the TextField
(e.g., via .disabled(isDisabled) or equivalent) and add a guard inside the
onSubmit handler to no-op when isDisabled is true (guard !isDisabled else {
return }), referencing the existing TextField, its onSubmit closure, the enter()
call, and the isDisabled flag so both UI interaction and programmatic submit are
blocked.
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/Treatment/Treatment/View/TreatmentFilterView.swift`:
- Around line 29-48: The list currently checks filteredTreatments.isEmpty which
makes an empty search show the full list; change the branching to check the
search state instead (use viewModel.searchText.isEmpty or the searchText
binding) so that when searchText is empty you iterate viewModel.treatments,
otherwise you always iterate viewModel.filteredTreatments (even if empty).
Update the conditional around the ForEach (the block that builds
TreatmentRowView using viewModel.isSelected and viewModel.addTreatment) to use
searchText.isEmpty as the condition and keep the same TreatmentRowView
parameters.
- Line 14: The TreatmentSearchBarTextField currently passes an empty enter
closure and sets isDisabled to !viewModel.searchText.isEmpty which disables the
button when there is input; update the call so enter invokes the intended action
on the view model (e.g., viewModel.performSearch() or viewModel.clearSearch())
and fix the isDisabled logic to reflect the intended state (for example use
viewModel.searchText.isEmpty to disable when there is no input, or remove the
isDisabled prop if the button should always be enabled). Ensure you reference
TreatmentSearchBarTextField, the enter closure, and viewModel.searchText when
making the change.
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/Treatment/Treatment/View/TreatmentView.swift`:
- Line 76: Update the SwiftLint MARK comment formatting for the
TreatmentSelectedCategoryView section: replace the current comment token that
reads "//MARK: - TreatmentSelectedCategoryView" with the correct spaced format
"// MARK: - TreatmentSelectedCategoryView" so it conforms to the SwiftLint rule;
locate the comment near the TreatmentSelectedCategoryView section in
TreatmentView.swift and adjust the spacing around the MARK token.
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/Treatment/Treatment/ViewModel/TreatmentViewModel.swift`:
- Around line 48-55: The isDateTextFieldNotEmpty() method currently only checks
numeric ranges for year/month/day allowing invalid combinations (e.g., 2/31);
update this function to parse year/month/day into Ints, check the year range
(>=2020) as before, then build DateComponents(year:yearInt, month:monthInt,
day:dayInt) and use Calendar.current.date(from:) to ensure the combination
yields a real Date (non-nil) before returning true; keep the existing guards for
empty strings and numeric parsing but replace the separate day-range guard with
this DateComponents/Calendar validation to reject impossible dates.
| TextField("" ,text: $text) | ||
| .foregroundStyle(.gray1000) | ||
| .typography(.body1_m_14) | ||
| .multilineTextAlignment(.leading) | ||
| .tint(.gray1000) | ||
| .onSubmit { | ||
| enter() | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
비활성 상태에서도 엔터 제출이 실행됩니다
isDisabled를 검색 비활성 의미로 쓰는 거라면, 버튼만 막는 것으론 onSubmit이 그대로 실행됩니다. 의도라면 TextField 비활성 + onSubmit 가드를 같이 두는 편이 안전합니다.
✅ 제안 수정
- TextField("" ,text: $text)
+ TextField("" ,text: $text)
.foregroundStyle(.gray1000)
.typography(.body1_m_14)
.multilineTextAlignment(.leading)
.tint(.gray1000)
+ .disabled(isDisabled)
.onSubmit {
- enter()
+ guard !isDisabled else { return }
+ enter()
}Also applies to: 45-45
🤖 Prompt for AI Agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/Treatment/Components/TreatmentSearchBarTextField.swift`
around lines 26 - 33, The TextField's onSubmit currently fires even when
searches are meant to be disabled; update the component to both disable the
TextField (e.g., via .disabled(isDisabled) or equivalent) and add a guard inside
the onSubmit handler to no-op when isDisabled is true (guard !isDisabled else {
return }), referencing the existing TextField, its onSubmit closure, the enter()
call, and the isDisabled flag so both UI interaction and programmatic submit are
blocked.
| @ObservedObject var viewModel: TreatmentViewModel | ||
| var body: some View { | ||
| VStack { | ||
| TreatmentSearchBarTextField(text: $viewModel.searchText, enter: { }, isDisabled: !viewModel.searchText.isEmpty) |
There was a problem hiding this comment.
검색 버튼이 사실상 무동작/비활성입니다
enter가 빈 클로저이고 isDisabled가 !searchText.isEmpty라 입력 시 버튼이 비활성됩니다. 버튼을 유지할 거라면 실제 동작(즉시 검색/클리어 등)과 비활성 조건을 의도에 맞게 조정해 주세요.
✅ 최소 수정 예시
- TreatmentSearchBarTextField(text: $viewModel.searchText, enter: { }, isDisabled: !viewModel.searchText.isEmpty)
+ TreatmentSearchBarTextField(
+ text: $viewModel.searchText,
+ enter: { viewModel.filterTreatments(by: viewModel.searchText) },
+ isDisabled: viewModel.searchText.isEmpty
+ )🤖 Prompt for AI Agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/Treatment/Treatment/View/TreatmentFilterView.swift`
at line 14, The TreatmentSearchBarTextField currently passes an empty enter
closure and sets isDisabled to !viewModel.searchText.isEmpty which disables the
button when there is input; update the call so enter invokes the intended action
on the view model (e.g., viewModel.performSearch() or viewModel.clearSearch())
and fix the isDisabled logic to reflect the intended state (for example use
viewModel.searchText.isEmpty to disable when there is no input, or remove the
isDisabled prop if the button should always be enabled). Ensure you reference
TreatmentSearchBarTextField, the enter closure, and viewModel.searchText when
making the change.
| if viewModel.filteredTreatments.isEmpty { | ||
| ForEach(viewModel.treatments, id: \.id) { treatment in | ||
| TreatmentRowView( | ||
| displayMode: .checkBoxView, | ||
| treatmentEntity: treatment, | ||
| isSelected: .constant(viewModel.isSelected(treatment)), | ||
| action: { viewModel.addTreatment(treatment) } | ||
| ) | ||
| } | ||
| } else { | ||
| ForEach(viewModel.filteredTreatments, id: \.id) { | ||
| treatment in | ||
| TreatmentRowView( | ||
| displayMode: .checkBoxView, | ||
| treatmentEntity: treatment, | ||
| isSelected: .constant(viewModel.isSelected(treatment)), | ||
| action: { viewModel.addTreatment(treatment) } | ||
| ) | ||
| } | ||
| } |
There was a problem hiding this comment.
검색 결과 0건이면 전체 리스트로 되돌아가는 버그
현재는 filteredTreatments.isEmpty일 때 전체 목록을 보여서, 검색 결과가 없는 경우에도 필터가 적용되지 않은 것처럼 보입니다. searchText로 분기해서 “검색 중 / 기본 상태”를 구분해야 합니다.
🐛 제안 수정
- if viewModel.filteredTreatments.isEmpty {
- ForEach(viewModel.treatments, id: \.id) { treatment in
+ if viewModel.searchText.isEmpty {
+ ForEach(viewModel.treatments, id: \.id) { treatment in
TreatmentRowView(
displayMode: .checkBoxView,
treatmentEntity: treatment,
isSelected: .constant(viewModel.isSelected(treatment)),
action: { viewModel.addTreatment(treatment) }
)
}
} else {
ForEach(viewModel.filteredTreatments, id: \.id) {
treatment in
TreatmentRowView(
displayMode: .checkBoxView,
treatmentEntity: treatment,
isSelected: .constant(viewModel.isSelected(treatment)),
action: { viewModel.addTreatment(treatment) }
)
}
}🤖 Prompt for AI Agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/Treatment/Treatment/View/TreatmentFilterView.swift`
around lines 29 - 48, The list currently checks filteredTreatments.isEmpty which
makes an empty search show the full list; change the branching to check the
search state instead (use viewModel.searchText.isEmpty or the searchText
binding) so that when searchText is empty you iterate viewModel.treatments,
otherwise you always iterate viewModel.filteredTreatments (even if empty).
Update the conditional around the ForEach (the block that builds
TreatmentRowView using viewModel.isSelected and viewModel.addTreatment) to use
searchText.isEmpty as the condition and keep the same TreatmentRowView
parameters.
| } | ||
| } | ||
|
|
||
| //MARK: - TreatmentSelectedCategoryView |
There was a problem hiding this comment.
MARK 주석 포맷을 SwiftLint 규칙에 맞게 수정하세요 (Line 76).
//MARK: → // MARK: - ... 형태가 규칙입니다.
🧹 제안 수정
-//MARK: - TreatmentSelectedCategoryView
+// MARK: - TreatmentSelectedCategoryView📝 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.
| //MARK: - TreatmentSelectedCategoryView | |
| // MARK: - TreatmentSelectedCategoryView |
🧰 Tools
🪛 SwiftLint (0.57.0)
[Warning] 76-76: MARK comment should be in valid format. e.g. '// MARK: ...' or '// MARK: - ...'
(mark)
🤖 Prompt for AI Agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/Treatment/Treatment/View/TreatmentView.swift`
at line 76, Update the SwiftLint MARK comment formatting for the
TreatmentSelectedCategoryView section: replace the current comment token that
reads "//MARK: - TreatmentSelectedCategoryView" with the correct spaced format
"// MARK: - TreatmentSelectedCategoryView" so it conforms to the SwiftLint rule;
locate the comment near the TreatmentSelectedCategoryView section in
TreatmentView.swift and adjust the spacing around the MARK token.
| func isDateTextFieldNotEmpty() -> Bool { | ||
| guard !year.isEmpty, !month.isEmpty, !day.isEmpty else { | ||
| return false | ||
| } | ||
| guard let yearInt = Int(year), yearInt >= 2020, | ||
| let monthInt = Int(month), monthInt >= 1, monthInt <= 12, | ||
| let dayInt = Int(day), dayInt >= 1, dayInt <= 31 else { | ||
| return false |
There was a problem hiding this comment.
캘린더 유효성 검증 부족으로 잘못된 날짜가 통과됩니다 (Line 48-55).
현재 로직은 월/일 조합 유효성을 확인하지 않아 2/31 같은 날짜도 통과합니다. 실제 날짜 유효성 검사를 추가하세요.
🛠️ 제안 수정
func isDateTextFieldNotEmpty() -> Bool {
- guard !year.isEmpty, !month.isEmpty, !day.isEmpty else {
- return false
- }
- guard let yearInt = Int(year), yearInt >= 2020,
- let monthInt = Int(month), monthInt >= 1, monthInt <= 12,
- let dayInt = Int(day), dayInt >= 1, dayInt <= 31 else {
- return false
- }
-
- return true
+ guard let yearInt = Int(year), yearInt >= 2020,
+ let monthInt = Int(month),
+ let dayInt = Int(day) else {
+ return false
+ }
+ var comps = DateComponents()
+ comps.year = yearInt
+ comps.month = monthInt
+ comps.day = dayInt
+ return Calendar.current.date(from: comps) != 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.
| func isDateTextFieldNotEmpty() -> Bool { | |
| guard !year.isEmpty, !month.isEmpty, !day.isEmpty else { | |
| return false | |
| } | |
| guard let yearInt = Int(year), yearInt >= 2020, | |
| let monthInt = Int(month), monthInt >= 1, monthInt <= 12, | |
| let dayInt = Int(day), dayInt >= 1, dayInt <= 31 else { | |
| return false | |
| func isDateTextFieldNotEmpty() -> Bool { | |
| guard let yearInt = Int(year), yearInt >= 2020, | |
| let monthInt = Int(month), | |
| let dayInt = Int(day) else { | |
| return false | |
| } | |
| var comps = DateComponents() | |
| comps.year = yearInt | |
| comps.month = monthInt | |
| comps.day = dayInt | |
| return Calendar.current.date(from: comps) != nil | |
| } |
🤖 Prompt for AI Agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/Treatment/Treatment/ViewModel/TreatmentViewModel.swift`
around lines 48 - 55, The isDateTextFieldNotEmpty() method currently only checks
numeric ranges for year/month/day allowing invalid combinations (e.g., 2/31);
update this function to parse year/month/day into Ints, check the year range
(>=2020) as before, then build DateComponents(year:yearInt, month:monthInt,
day:dayInt) and use Calendar.current.date(from:) to ensure the combination
yields a real Date (non-nil) before returning true; keep the existing guards for
empty strings and numeric parsing but replace the separate day-range guard with
this DateComponents/Calendar validation to reject impossible dates.

🔗 연결된 이슈
📄 작업 내용
👀 기타 더 이야기해볼 점
하다보니까 공통 컴포넌트 사용할 때 protocol로 좀 더 코드를 줄일 수 있지 않을까 라는 생각이 들었습니다.
