diff --git a/HotSpot/Sources/Presentation/Coordinator/AppCoordinatorView.swift b/HotSpot/Sources/Presentation/Coordinator/AppCoordinatorView.swift index 3ccc38b..160b678 100644 --- a/HotSpot/Sources/Presentation/Coordinator/AppCoordinatorView.swift +++ b/HotSpot/Sources/Presentation/Coordinator/AppCoordinatorView.swift @@ -24,7 +24,7 @@ struct AppCoordinatorView: View { destination: IfLetStore( store.scope(state: \.search, action: \.search), then: { store in - SearchView(store: store) + SearchView(store: store, coordinatorStore: self.store) .background(searchToDetailNavigationLink(viewStore: viewStore)) } ), diff --git a/HotSpot/Sources/Presentation/Search/Component/SearchFilterView.swift b/HotSpot/Sources/Presentation/Search/Component/SearchFilterView.swift new file mode 100644 index 0000000..23ab5be --- /dev/null +++ b/HotSpot/Sources/Presentation/Search/Component/SearchFilterView.swift @@ -0,0 +1,141 @@ +import SwiftUI +import ComposableArchitecture + +struct SearchFilterView: View { + let store: StoreOf + let coordinatorStore: StoreOf + + var body: some View { + WithViewStore(store, observe: { $0 }) { viewStore in + WithViewStore(coordinatorStore, observe: { $0 }) { coordinatorViewStore in + NavigationView { + FilterForm(viewStore: viewStore, coordinatorViewStore: coordinatorViewStore) + } + } + } + } +} + +private struct FilterForm: View { + let viewStore: ViewStore + let coordinatorViewStore: ViewStore + + var body: some View { + Form { + BudgetSection(viewStore: viewStore) + FeaturesSection(viewStore: viewStore) + CuisineSection(viewStore: viewStore) + DistanceSection(viewStore: viewStore) + } + .navigationTitle("Filter") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button("Reset") { + viewStore.send(.resetFilters) + } + } + ToolbarItem(placement: .navigationBarTrailing) { + Button("Apply") { + viewStore.send(.search(viewStore.searchText)) + viewStore.send(.toggleFilterSheet) + } + } + } + } +} + +private struct BudgetSection: View { + let viewStore: ViewStore + + var body: some View { + Section(header: Text("Budget")) { + Picker("Budget", selection: viewStore.binding( + get: \.selectedBudget, + send: SearchStore.Action.updateBudget + )) { + Text("Any").tag(0) + Text("¥1,000~").tag(1) + Text("¥3,000~").tag(2) + Text("¥5,000~").tag(3) + Text("¥10,000~").tag(4) + } + } + } +} + +private struct FeaturesSection: View { + let viewStore: ViewStore + + var body: some View { + Section(header: Text("Features")) { + Toggle("WiFi Available", isOn: viewStore.binding( + get: \.hasWiFi, + send: SearchStore.Action.toggleWiFi + )) + Toggle("Private Room", isOn: viewStore.binding( + get: \.hasPrivateRoom, + send: SearchStore.Action.togglePrivateRoom + )) + Toggle("Non-Smoking", isOn: viewStore.binding( + get: \.isNonSmoking, + send: SearchStore.Action.toggleNonSmoking + )) + Toggle("Parking Available", isOn: viewStore.binding( + get: \.hasParking, + send: SearchStore.Action.toggleParking + )) + } + } +} + +private struct CuisineSection: View { + let viewStore: ViewStore + + var body: some View { + Section(header: Text("Cuisine")) { + Picker("Cuisine", selection: viewStore.binding( + get: \.selectedCuisine, + send: SearchStore.Action.updateCuisine + )) { + Text("Any").tag(0) + Text("Japanese").tag(1) + Text("Italian").tag(2) + Text("French").tag(3) + Text("Chinese").tag(4) + } + } + } +} + +private struct DistanceSection: View { + let viewStore: ViewStore + + var body: some View { + Section(header: Text("Distance")) { + Picker("Distance", selection: viewStore.binding( + get: \.selectedDistance, + send: SearchStore.Action.updateDistance + )) { + Text("300m").tag(1) + Text("500m").tag(2) + Text("1km").tag(3) + Text("2km").tag(4) + Text("3km").tag(5) + } + } + } +} + +#Preview { + SearchFilterView( + store: Store( + initialState: SearchStore.State(), + reducer: { SearchStore() } + ), + coordinatorStore: Store( + initialState: AppCoordinator.State(), + reducer: { AppCoordinator() } + ) + ) +} \ No newline at end of file diff --git a/HotSpot/Sources/Presentation/Search/SearchStore.swift b/HotSpot/Sources/Presentation/Search/SearchStore.swift index a9d0b1d..6d3b812 100644 --- a/HotSpot/Sources/Presentation/Search/SearchStore.swift +++ b/HotSpot/Sources/Presentation/Search/SearchStore.swift @@ -14,6 +14,16 @@ struct SearchStore { var currentLocation: CLLocationCoordinate2D? var selectedShop: ShopModel? = nil var paginationState: PaginationState = .init() + var isFilterSheetPresented: Bool = false + + // Filter states + var selectedBudget: Int = 0 + var hasWiFi: Bool = false + var hasPrivateRoom: Bool = false + var isNonSmoking: Bool = false + var hasParking: Bool = false + var selectedCuisine: Int = 0 + var selectedDistance: Int = 3 static func == (lhs: State, rhs: State) -> Bool { lhs.shops == rhs.shops && @@ -24,7 +34,15 @@ struct SearchStore { lhs.selectedShop == rhs.selectedShop && lhs.paginationState.currentPage == rhs.paginationState.currentPage && lhs.paginationState.isLastPage == rhs.paginationState.isLastPage && - lhs.paginationState.isLoading == rhs.paginationState.isLoading + lhs.paginationState.isLoading == rhs.paginationState.isLoading && + lhs.isFilterSheetPresented == rhs.isFilterSheetPresented && + lhs.selectedBudget == rhs.selectedBudget && + lhs.hasWiFi == rhs.hasWiFi && + lhs.hasPrivateRoom == rhs.hasPrivateRoom && + lhs.isNonSmoking == rhs.isNonSmoking && + lhs.hasParking == rhs.hasParking && + lhs.selectedCuisine == rhs.selectedCuisine && + lhs.selectedDistance == rhs.selectedDistance } } @@ -38,6 +56,17 @@ struct SearchStore { case handleError(Error) case loadMore case updatePaginationState(PaginationState) + + // Filter actions + case toggleFilterSheet + case updateBudget(Int) + case toggleWiFi + case togglePrivateRoom + case toggleNonSmoking + case toggleParking + case updateCuisine(Int) + case updateDistance(Int) + case resetFilters } var body: some ReducerOf { @@ -82,16 +111,16 @@ struct SearchStore { let request = ShopSearchRequestDTO( lat: location.latitude, lng: location.longitude, - range: 5, + range: state.selectedDistance, count: nil, keyword: text, - genre: nil, + genre: state.selectedCuisine > 0 ? String(state.selectedCuisine) : nil, order: nil, start: nil, - budget: nil, - privateRoom: nil, - wifi: nil, - nonSmoking: nil, + budget: state.selectedBudget > 0 ? String(state.selectedBudget) : nil, + privateRoom: state.hasPrivateRoom ? true : nil, + wifi: state.hasWiFi ? true : nil, + nonSmoking: state.isNonSmoking ? true : nil, coupon: nil, openNow: nil ) @@ -130,16 +159,16 @@ struct SearchStore { let request = ShopSearchRequestDTO( lat: location.latitude, lng: location.longitude, - range: 5, + range: state.selectedDistance, count: nil, keyword: state.searchText, - genre: nil, + genre: state.selectedCuisine > 0 ? String(state.selectedCuisine) : nil, order: nil, start: nil, - budget: nil, - privateRoom: nil, - wifi: nil, - nonSmoking: nil, + budget: state.selectedBudget > 0 ? String(state.selectedBudget) : nil, + privateRoom: state.hasPrivateRoom ? true : nil, + wifi: state.hasWiFi ? true : nil, + nonSmoking: state.isNonSmoking ? true : nil, coupon: nil, openNow: nil ) @@ -178,6 +207,49 @@ struct SearchStore { state.paginationState = newState print("PaginationState updated - currentPage: \(newState.currentPage), isLastPage: \(newState.isLastPage), isLoading: \(newState.isLoading)") return .none + + // Filter actions + case .toggleFilterSheet: + state.isFilterSheetPresented.toggle() + return .none + + case let .updateBudget(budget): + state.selectedBudget = budget + return .none + + case .toggleWiFi: + state.hasWiFi.toggle() + return .none + + case .togglePrivateRoom: + state.hasPrivateRoom.toggle() + return .none + + case .toggleNonSmoking: + state.isNonSmoking.toggle() + return .none + + case .toggleParking: + state.hasParking.toggle() + return .none + + case let .updateCuisine(cuisine): + state.selectedCuisine = cuisine + return .none + + case let .updateDistance(distance): + state.selectedDistance = distance + return .none + + case .resetFilters: + state.selectedBudget = 0 + state.hasWiFi = false + state.hasPrivateRoom = false + state.isNonSmoking = false + state.hasParking = false + state.selectedCuisine = 0 + state.selectedDistance = 3 + return .none } } } diff --git a/HotSpot/Sources/Presentation/Search/SearchView.swift b/HotSpot/Sources/Presentation/Search/SearchView.swift index 072f7d0..226103a 100644 --- a/HotSpot/Sources/Presentation/Search/SearchView.swift +++ b/HotSpot/Sources/Presentation/Search/SearchView.swift @@ -6,44 +6,58 @@ import ComposableArchitecture struct SearchView: View { let store: StoreOf + let coordinatorStore: StoreOf @State private var isSearchFocused = false var body: some View { WithViewStore(store, observe: { $0 }) { viewStore in - VStack(spacing: 0) { - if !isSearchFocused { - TopBarView( - leftSide: .left, - leftAction: { - viewStore.send(.pop) + WithViewStore(coordinatorStore, observe: { $0 }) { coordinatorViewStore in + VStack(spacing: 0) { + if !isSearchFocused { + TopBarView( + leftSide: .left, + leftAction: { + viewStore.send(.pop) + }, + rightSide: .icon, + rightIcon: UIImage.icMore, + rightAction: { + viewStore.send(.toggleFilterSheet) + } + ) + } + + SearchBar( + searchText: viewStore.searchText, + onSearch: { viewStore.send(.search($0)) }, + isSearchFocused: $isSearchFocused + ) + + SearchResults( + error: viewStore.error, + searchText: viewStore.searchText, + shops: viewStore.shops, + onSelectShop: { viewStore.send(.selectShop($0)) }, + onLoadMore: { + if !viewStore.paginationState.isLastPage { + viewStore.send(.loadMore) + } } ) } - - SearchBar( - searchText: viewStore.searchText, - onSearch: { viewStore.send(.search($0)) }, - isSearchFocused: $isSearchFocused - ) - - SearchResults( - error: viewStore.error, - searchText: viewStore.searchText, - shops: viewStore.shops, - onSelectShop: { viewStore.send(.selectShop($0)) }, - onLoadMore: { - if !viewStore.paginationState.isLastPage { - viewStore.send(.loadMore) - } - } - ) - } - .navigationBarHidden(true) - .onAppear { - viewStore.send(.onAppear) - } - .onTapGesture { - UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) + .navigationBarHidden(true) + .onAppear { + viewStore.send(.onAppear) + } + .onTapGesture { + UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) + } + .sheet(isPresented: viewStore.binding( + get: \.isFilterSheetPresented, + send: SearchStore.Action.toggleFilterSheet + )) { + SearchFilterView(store: store, coordinatorStore: coordinatorStore) + } } } } @@ -54,6 +68,10 @@ struct SearchView: View { store: Store( initialState: SearchStore.State(), reducer: { SearchStore() } + ), + coordinatorStore: Store( + initialState: AppCoordinator.State(), + reducer: { AppCoordinator() } ) ) }