diff --git a/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/Treatment/Components/SelectedTreatmentView.swift b/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/Treatment/Components/SelectedTreatmentView.swift index 055b45c1..360103a4 100644 --- a/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/Treatment/Components/SelectedTreatmentView.swift +++ b/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/Treatment/Components/SelectedTreatmentView.swift @@ -8,14 +8,15 @@ import SwiftUI struct SelectedTreatmentView: View { - @ObservedObject var viewModel: NoTreatmentViewModel + let selectedTreatments: [TreatmentEntity] + let removeTreatment: (TreatmentEntity) -> Void private let itemHeight: CGFloat = 34.adjustedH private let spacing: CGFloat = 16.adjustedH private let maxVisibleCount = 3 private var scrollViewHeight: CGFloat { - let count = min(viewModel.selectedTreatments.count, maxVisibleCount) + let count = min(selectedTreatments.count, maxVisibleCount) let contentHeight = CGFloat(count) * itemHeight + CGFloat(max(count - 1, 0)) * spacing return contentHeight + 24.adjustedH } @@ -47,12 +48,12 @@ struct SelectedTreatmentView: View { ) ScrollView(.vertical, showsIndicators: false) { VStack(spacing: spacing) { - ForEach(viewModel.selectedTreatments, id: \.id) { treatment in + ForEach(selectedTreatments, id: \.id) { treatment in TreatmentRowView( displayMode: .summary, treatmentEntity: treatment, isSelected: .constant(true), - action: { viewModel.removeTreatment(treatment) } + action: { removeTreatment(treatment) } ) .frame(height: itemHeight) } @@ -60,7 +61,7 @@ struct SelectedTreatmentView: View { .padding(.vertical, 14.adjustedH) } .frame(height: scrollViewHeight) - .scrollDisabled(viewModel.selectedTreatments.count <= maxVisibleCount) + .scrollDisabled(selectedTreatments.count <= maxVisibleCount) .padding(.horizontal, 24.5.adjustedW) } } diff --git a/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/Treatment/Components/TreatmentSearchBarTextField.swift b/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/Treatment/Components/TreatmentSearchBarTextField.swift new file mode 100644 index 00000000..6da2cfd1 --- /dev/null +++ b/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/Treatment/Components/TreatmentSearchBarTextField.swift @@ -0,0 +1,54 @@ +// +// TreatmentSearchBarTextField.swift +// Cherrish-iOS +// +// Created by 어재선 on 1/16/26. +// + +import SwiftUI + +struct TreatmentSearchBarTextField: View { + @Binding var text: String + let enter: () -> Void + var isDisabled: Bool + var body: some View { + + VStack { + + HStack{ + ZStack { + if text.isEmpty { + HStack{ + TypographyText("원하시는 시술을 적어주세요.", style: .body1_r_14, color: .gray600) + Spacer() + } + } + TextField("" ,text: $text) + .foregroundStyle(.gray1000) + .typography(.body1_m_14) + .multilineTextAlignment(.leading) + .tint(.gray1000) + .onSubmit { + enter() + } + } + .padding(.vertical, 8.adjustedH) + .padding(.leading, 16.5.adjustedW) + Button{ + enter() + } label: { + ZStack { + Image(.search) + } + } + .padding(.leading, 8) + .disabled(isDisabled) + } + .padding(.trailing, 16.5.adjustedW) + } + .background{ + RoundedRectangle(cornerRadius: 30) + .foregroundStyle(.gray200) + } + } +} diff --git a/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/Treatment/NoTreatment/View/NoTreatmentView.swift b/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/Treatment/NoTreatment/View/NoTreatmentView.swift index b50168ed..7f1fc4b5 100644 --- a/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/Treatment/NoTreatment/View/NoTreatmentView.swift +++ b/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/Treatment/NoTreatment/View/NoTreatmentView.swift @@ -61,10 +61,16 @@ struct NoTreatmentView: View { } } Spacer() - - CherrishButton(title: "다음", type: .next, state: .constant(viewModel.canProceed ? .active : .normal)) { - viewModel.next() - + Group { + if viewModel.state == .treatmentFilter { + if !viewModel.selectedTreatments.isEmpty { + SelectedTreatmentView(selectedTreatments: viewModel.selectedTreatments, removeTreatment: viewModel.removeTreatment(_:)) + } + } + CherrishButton(title: "다음", type: .next, state: .constant(viewModel.canProceed ? .active : .normal)) { + viewModel.next() + } + .padding(.horizontal, 25.adjustedW) } .padding(.leading, 25.adjustedW) .padding(.trailing, 24.adjustedW) diff --git a/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/Treatment/Treatment/Model/Treatment.swift b/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/Treatment/Treatment/Model/Treatment.swift new file mode 100644 index 00000000..9846e723 --- /dev/null +++ b/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/Treatment/Treatment/Model/Treatment.swift @@ -0,0 +1,53 @@ +// +// Treatment.swift +// Cherrish-iOS +// +// Created by 어재선 on 1/16/26. +// + +import Foundation + +enum Treatment: Int, CaseIterable, Identifiable { + case targetDdaySetting = 1 + case treatmentFilter + case downTimeSetting + + var id: Self { self } + + var title: String { + switch self { + case .targetDdaySetting: + return "목표 디데이 설정" + case .treatmentFilter: + return "시술 필터링" + case .downTimeSetting: + return "다운타임 설정" + } + } + + var isFirst: Bool { self == Self.allCases.first } + var isLast: Bool { self == Self.allCases.last } +} + +// MARK: - Navigation +extension Treatment { + mutating func next() { + let allCases = Self.allCases + + guard let currentIndex = allCases.firstIndex(of: self), + currentIndex + 1 < allCases.count else { return } + + self = allCases[allCases.index(after: currentIndex)] + + } + + mutating func previous() { + let allCases = Self.allCases + + guard let currentIndex = allCases.firstIndex(of: self), + currentIndex > 0 else { return } + + self = allCases[allCases.index(before: currentIndex)] + + } +} diff --git a/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/Treatment/Treatment/View/TreatmentFilterView.swift b/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/Treatment/Treatment/View/TreatmentFilterView.swift new file mode 100644 index 00000000..3f2f7c4d --- /dev/null +++ b/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/Treatment/Treatment/View/TreatmentFilterView.swift @@ -0,0 +1,54 @@ +// +// TreatmentFilterView.swift +// Cherrish-iOS +// +// Created by 어재선 on 1/16/26. +// + +import SwiftUI + +struct TreatmentFilterView: View { + @ObservedObject var viewModel: TreatmentViewModel + var body: some View { + VStack { + TreatmentSearchBarTextField(text: $viewModel.searchText, enter: { }, isDisabled: !viewModel.searchText.isEmpty) + + ScrollView(.vertical, showsIndicators: false) { + HStack(alignment: .top,spacing: 4) { + TypographyText("◎", style: .body3_r_12, color: .gray600) + + VStack(alignment: .leading, spacing: 0) { + TypographyText("본 정보는 인터넷 빅테이터 검색 및 분석을 통해 수집된 정보이며,공식적인 의료 정보가 아닙니다. ", style: .body3_r_12, color: .gray600) + .lineLimit(2) + + } + + Spacer() + + } + 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) } + ) + } + } + } + + } + .padding(.horizontal, 24.5.adjustedW) + } +} diff --git a/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/Treatment/Treatment/View/TreatmentView.swift b/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/Treatment/Treatment/View/TreatmentView.swift new file mode 100644 index 00000000..ba882ae7 --- /dev/null +++ b/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/Treatment/Treatment/View/TreatmentView.swift @@ -0,0 +1,132 @@ +// +// TreatmentView.swift +// Cherrish-iOS +// +// Created by 어재선 on 1/16/26. +// + +import SwiftUI + +struct TreatmentView: View { + @StateObject private var viewModel: TreatmentViewModel + + init(viewModel: TreatmentViewModel = TreatmentViewModel()) { + _viewModel = StateObject(wrappedValue: viewModel) + } + var body: some View { + VStack { + CherrishNavigationBar( + title:viewModel.state.title, + leftButtonAction: { + viewModel.previous() + }, + rightButtonAction: { + + } + ) + + ProgressBar( + totalSteps: Treatment.allCases.count, + currentStep: .constant(viewModel.step) + ) + .padding( .horizontal, 33.5.adjustedW) + + VStack(spacing: 0){ + Group { + switch viewModel.state { + case .targetDdaySetting: + TargetDdaySettingView(dDayState: $viewModel.dDay, year: $viewModel.year, month: $viewModel.month, day: $viewModel.day) + .padding( .leading, 34.adjustedW) + .padding( .trailing, 33.adjustedW) + .id(viewModel.state) + case .treatmentFilter: + TreatmentFilterView(viewModel: viewModel) + case .downTimeSetting: + DownTimeSettingView(treatments: viewModel.selectedTreatments) + } + } + + Spacer() + + Group { + if viewModel.state == .treatmentFilter { + if !viewModel.selectedTreatments.isEmpty { + SelectedTreatmentView(selectedTreatments: viewModel.selectedTreatments, removeTreatment: viewModel.removeTreatment(_:)) + } + } + + CherrishButton(title: "다음", type: .next, state: .constant(viewModel.canProceed ? .active : .normal)) { + viewModel.next() + + } + .padding(.horizontal, 25.adjustedW) + + } + + Spacer() + .frame(height: 38.adjustedH) + + } + .id(viewModel.step) + } + .ignoresSafeArea(.keyboard) + } +} + +//MARK: - TreatmentSelectedCategoryView + +private struct TreatmentSelectedCategory: View { + @ObservedObject var viewModel: NoTreatmentViewModel + let columns = [ + GridItem(.flexible()), + GridItem(.flexible()) + ] + var body: some View { + VStack { + Spacer() + .frame(height: 70.adjustedH) + HStack(spacing: 0) { + VStack(alignment: .leading, spacing: 0) { + TypographyText( + "요즘 가장 신경 쓰이는 ", + style: .title1_sb_18, + color: .gray1000 + ) + TypographyText( + "피부 고민은 무엇인가요?", + style: .title1_sb_18, + color: .gray1000 + ) + TypographyText( + "선택한 고민을 기준으로 시술 정보를 정리해줘요.", + style: .body1_m_14, + color: .gray700 + ) + } + Spacer() + } + Spacer() + .frame(height: 40.adjustedH) + LazyVGrid(columns: columns, spacing: 12) { + ForEach(TreatmentCategory.allCases, id: \.id) { catagory in + SelectionChip( + title: catagory.title, + isSelected: Binding( + get: { + viewModel.treatmentCatagory == catagory + }, + set: { + isSelected in + guard isSelected else { + return + } + viewModel.treatmentCatagory = catagory + } + ) + ) + + } + } + } + } +} diff --git a/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/Treatment/Treatment/ViewModel/TreatmentViewModel+Filter.swift b/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/Treatment/Treatment/ViewModel/TreatmentViewModel+Filter.swift new file mode 100644 index 00000000..b7ce7fd1 --- /dev/null +++ b/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/Treatment/Treatment/ViewModel/TreatmentViewModel+Filter.swift @@ -0,0 +1,23 @@ +// +// TreatmentViewModel+Filter.swift +// Cherrish-iOS +// +// Created by 어재선 on 1/16/26. +// + +import Foundation + +extension TreatmentViewModel { + + func addTreatment(_ treatment: TreatmentEntity) { + guard !isSelected(treatment) else { return } + selectedTreatments.append(treatment) + } + + func removeTreatment(_ treatment: TreatmentEntity) { + selectedTreatments.removeAll { $0.id == treatment.id } + } + func isSelected(_ treatment: TreatmentEntity) -> Bool { + selectedTreatments.contains(where: { $0.id == treatment.id }) + } +} diff --git a/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/Treatment/Treatment/ViewModel/TreatmentViewModel+Search.swift b/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/Treatment/Treatment/ViewModel/TreatmentViewModel+Search.swift new file mode 100644 index 00000000..bac3eaa7 --- /dev/null +++ b/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/Treatment/Treatment/ViewModel/TreatmentViewModel+Search.swift @@ -0,0 +1,36 @@ +// +// TreatmentViewModel+Search.swift +// Cherrish-iOS +// +// Created by 어재선 on 1/17/26. +// + +import Combine +import Foundation + +extension TreatmentViewModel { + + func setupSearch() { + $searchText + .debounce(for: .milliseconds(300), scheduler: RunLoop.main) + .removeDuplicates() + .sink { [weak self] text in + self?.filterTreatments(by: text) + } + .store(in: &cancellables) + } + + func filterTreatments(by text: String) { + if text.isEmpty { + filteredTreatments = treatments + } else { + filteredTreatments = treatments.filter { + $0.name.localizedCaseInsensitiveContains(text) + } + } + } + + func clearSearch() { + searchText = "" + } +} diff --git a/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/Treatment/Treatment/ViewModel/TreatmentViewModel.swift b/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/Treatment/Treatment/ViewModel/TreatmentViewModel.swift new file mode 100644 index 00000000..38b68709 --- /dev/null +++ b/Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Calendar/Treatment/Treatment/ViewModel/TreatmentViewModel.swift @@ -0,0 +1,63 @@ +// +// TreatmentViewModel.swift +// Cherrish-iOS +// +// Created by 어재선 on 1/16/26. +// + +import SwiftUI +import Combine + +final class TreatmentViewModel: ObservableObject{ + @Published var state: Treatment = .targetDdaySetting + var step: Int { state.rawValue } + @Published var dDay: DdayState? + @Published var year: String = "" + @Published var month: String = "" + @Published var day: String = "" + @Published var treatments: [TreatmentEntity] = TreatmentEntity.mockData + @Published var selectedTreatments: [TreatmentEntity] = [] + @Published var searchText = "" + @Published var filteredTreatments: [TreatmentEntity] = [] + + var cancellables = Set() + + init() { + filteredTreatments = treatments + setupSearch() + } + var canProceed: Bool { + switch state { + case .targetDdaySetting: + return isDateTextFieldNotEmpty() + case .treatmentFilter: + return !selectedTreatments.isEmpty + case .downTimeSetting: + return true + } + } + + func next() { + state.next() + } + + func previous() { + state.previous() + } + + 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 + + } + + +}