From afcf756cdff5c2db2444e21c4d5df1891035e8b4 Mon Sep 17 00:00:00 2001 From: coby5502 Date: Mon, 21 Apr 2025 13:38:02 +0900 Subject: [PATCH 01/32] [FEAT] error alert --- .../Model}/ShopGenre.swift | 0 .../Coordinator/AppCoordinator.swift | 27 +++++++++++++++++++ .../Sources/Presentation/Map/MapStore.swift | 16 +++++++++-- .../Sources/Presentation/Map/MapView.swift | 6 +++++ .../Search/Component/SearchResults.swift | 4 +-- .../Presentation/Search/SearchStore.swift | 16 +++++++++-- .../Presentation/Search/SearchView.swift | 6 +++++ 7 files changed, 69 insertions(+), 6 deletions(-) rename HotSpot/Sources/{Common/Extensions => Domain/Model}/ShopGenre.swift (100%) diff --git a/HotSpot/Sources/Common/Extensions/ShopGenre.swift b/HotSpot/Sources/Domain/Model/ShopGenre.swift similarity index 100% rename from HotSpot/Sources/Common/Extensions/ShopGenre.swift rename to HotSpot/Sources/Domain/Model/ShopGenre.swift diff --git a/HotSpot/Sources/Presentation/Coordinator/AppCoordinator.swift b/HotSpot/Sources/Presentation/Coordinator/AppCoordinator.swift index a6ec5ea..1af51df 100644 --- a/HotSpot/Sources/Presentation/Coordinator/AppCoordinator.swift +++ b/HotSpot/Sources/Presentation/Coordinator/AppCoordinator.swift @@ -5,6 +5,7 @@ import ComposableArchitecture final class AppCoordinator { private let window: UIWindow private let navigationController: UINavigationController + private var errorAlertController: UIAlertController? init(window: UIWindow) { self.window = window @@ -27,6 +28,32 @@ final class AppCoordinator { window.makeKeyAndVisible() } + func showError(_ error: ShopError) { + let errorMessage = ShopErrorMessageMapper.message(for: error) + showErrorAlert(message: errorMessage) + } + + private func showErrorAlert(message: String) { + errorAlertController?.dismiss(animated: false) + + let alert = UIAlertController( + title: "エラー", + message: message, + preferredStyle: .alert + ) + + alert.addAction(UIAlertAction( + title: "OK", + style: .default, + handler: { [weak self] _ in + self?.errorAlertController = nil + } + )) + + errorAlertController = alert + navigationController.present(alert, animated: true) + } + func showSearch() { let searchView = SearchView( store: Store( diff --git a/HotSpot/Sources/Presentation/Map/MapStore.swift b/HotSpot/Sources/Presentation/Map/MapStore.swift index 1f0734b..7aa1d0e 100644 --- a/HotSpot/Sources/Presentation/Map/MapStore.swift +++ b/HotSpot/Sources/Presentation/Map/MapStore.swift @@ -14,8 +14,8 @@ struct MapStore { center: CLLocationCoordinate2D(latitude: 35.6762, longitude: 139.6503), span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01) ) - var error: String? = nil var lastFetchedLocation: CLLocationCoordinate2D? = nil + var error: ShopError? = nil } enum Action { @@ -23,6 +23,7 @@ struct MapStore { case fetchShops case updateShops([ShopModel]) case handleError(Error) + case clearError } var body: some ReducerOf { @@ -59,7 +60,18 @@ struct MapStore { return .none case let .handleError(error): - state.error = error.localizedDescription + switch error { + case is URLError: + state.error = .network + case is DecodingError: + state.error = .decoding + default: + state.error = .server(message: error.localizedDescription) + } + return .none + + case .clearError: + state.error = nil return .none } } diff --git a/HotSpot/Sources/Presentation/Map/MapView.swift b/HotSpot/Sources/Presentation/Map/MapView.swift index 893e5db..793d2be 100644 --- a/HotSpot/Sources/Presentation/Map/MapView.swift +++ b/HotSpot/Sources/Presentation/Map/MapView.swift @@ -55,6 +55,12 @@ struct MapView: View { .padding(.bottom, 30) } } + .onChange(of: viewStore.error) { error in + if let error = error { + coordinator?.showError(error) + viewStore.send(.clearError) + } + } } } diff --git a/HotSpot/Sources/Presentation/Search/Component/SearchResults.swift b/HotSpot/Sources/Presentation/Search/Component/SearchResults.swift index 9526797..196d873 100644 --- a/HotSpot/Sources/Presentation/Search/Component/SearchResults.swift +++ b/HotSpot/Sources/Presentation/Search/Component/SearchResults.swift @@ -3,7 +3,7 @@ import CobyDS import Kingfisher struct SearchResults: View { - let error: String? + let error: ShopError? let searchText: String let shops: [ShopModel] let onSelectShop: (ShopModel) -> Void @@ -13,7 +13,7 @@ struct SearchResults: View { var body: some View { Group { if let error = error { - Text(error) + Text(ShopErrorMessageMapper.message(for: error)) .foregroundColor(.red) } else if searchText.isEmpty { EmptyResults(searchText: searchText) diff --git a/HotSpot/Sources/Presentation/Search/SearchStore.swift b/HotSpot/Sources/Presentation/Search/SearchStore.swift index bb1e0d2..d1e39af 100644 --- a/HotSpot/Sources/Presentation/Search/SearchStore.swift +++ b/HotSpot/Sources/Presentation/Search/SearchStore.swift @@ -10,7 +10,7 @@ struct SearchStore { struct State: Equatable { var shops: [ShopModel] = [] var searchText: String = "" - var error: String? = nil + var error: ShopError? = nil var currentLocation: CLLocationCoordinate2D? var paginationState: PaginationState = .init() @@ -33,6 +33,7 @@ struct SearchStore { case updateLocation(CLLocationCoordinate2D) case updateShops([ShopModel]) case handleError(Error) + case clearError case loadMore case updatePaginationState(PaginationState) case updateFilterState(SearchFilterStore.State) @@ -57,7 +58,18 @@ struct SearchStore { return .none case let .handleError(error): - state.error = error.localizedDescription + switch error { + case is URLError: + state.error = .network + case is DecodingError: + state.error = .decoding + default: + state.error = .server(message: error.localizedDescription) + } + return .none + + case .clearError: + state.error = nil return .none case let .search(text): diff --git a/HotSpot/Sources/Presentation/Search/SearchView.swift b/HotSpot/Sources/Presentation/Search/SearchView.swift index 5123534..31fc940 100644 --- a/HotSpot/Sources/Presentation/Search/SearchView.swift +++ b/HotSpot/Sources/Presentation/Search/SearchView.swift @@ -46,6 +46,12 @@ struct SearchView: View { .onAppear { viewStore.send(.onAppear) } + .onChange(of: viewStore.error) { error in + if let error = error { + coordinator?.showError(error) + viewStore.send(.clearError) + } + } .onTapGesture { UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) } From d82d0cd5db958d8555d57c01abaa38c4962ff6f2 Mon Sep 17 00:00:00 2001 From: coby5502 Date: Mon, 21 Apr 2025 13:56:54 +0900 Subject: [PATCH 02/32] [FEAT] shop empty message --- .../Coordinator/AppCoordinator.swift | 21 +++++++++++++++---- .../Sources/Presentation/Map/MapStore.swift | 2 +- .../Sources/Presentation/Map/MapView.swift | 8 +++++++ 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/HotSpot/Sources/Presentation/Coordinator/AppCoordinator.swift b/HotSpot/Sources/Presentation/Coordinator/AppCoordinator.swift index 1af51df..332d314 100644 --- a/HotSpot/Sources/Presentation/Coordinator/AppCoordinator.swift +++ b/HotSpot/Sources/Presentation/Coordinator/AppCoordinator.swift @@ -6,6 +6,7 @@ final class AppCoordinator { private let window: UIWindow private let navigationController: UINavigationController private var errorAlertController: UIAlertController? + private var messageAlertController: UIAlertController? init(window: UIWindow) { self.window = window @@ -30,14 +31,20 @@ final class AppCoordinator { func showError(_ error: ShopError) { let errorMessage = ShopErrorMessageMapper.message(for: error) - showErrorAlert(message: errorMessage) + showAlert(title: "エラー", message: errorMessage) } - private func showErrorAlert(message: String) { + func showMessage(title: String, message: String) { + showAlert(title: title, message: message) + } + + private func showAlert(title: String, message: String) { + // 이미 표시된 알림이 있다면 제거 errorAlertController?.dismiss(animated: false) + messageAlertController?.dismiss(animated: false) let alert = UIAlertController( - title: "エラー", + title: title, message: message, preferredStyle: .alert ) @@ -47,10 +54,16 @@ final class AppCoordinator { style: .default, handler: { [weak self] _ in self?.errorAlertController = nil + self?.messageAlertController = nil } )) - errorAlertController = alert + if title == "エラー" { + errorAlertController = alert + } else { + messageAlertController = alert + } + navigationController.present(alert, animated: true) } diff --git a/HotSpot/Sources/Presentation/Map/MapStore.swift b/HotSpot/Sources/Presentation/Map/MapStore.swift index 7aa1d0e..1df722b 100644 --- a/HotSpot/Sources/Presentation/Map/MapStore.swift +++ b/HotSpot/Sources/Presentation/Map/MapStore.swift @@ -31,13 +31,13 @@ struct MapStore { switch action { case let .updateRegion(region): state.region = region + state.visibleShops = filterVisibleShops(state.shops, in: region) if shouldFetchNewData(state: state, newRegion: region) { state.lastFetchedLocation = region.center return .send(.fetchShops) } - state.visibleShops = filterVisibleShops(state.shops, in: region) return .none case .fetchShops: diff --git a/HotSpot/Sources/Presentation/Map/MapView.swift b/HotSpot/Sources/Presentation/Map/MapView.swift index 793d2be..5cbea0c 100644 --- a/HotSpot/Sources/Presentation/Map/MapView.swift +++ b/HotSpot/Sources/Presentation/Map/MapView.swift @@ -61,6 +61,14 @@ struct MapView: View { viewStore.send(.clearError) } } + .onChange(of: viewStore.visibleShops) { shops in + if shops.isEmpty { + coordinator?.showMessage( + title: "お店が見つかりません", + message: "ズームインして再度お試しください" + ) + } + } } } From da05825b7bdf4da78c07fb75ea49c2c463cd185a Mon Sep 17 00:00:00 2001 From: coby5502 Date: Mon, 21 Apr 2025 14:02:38 +0900 Subject: [PATCH 03/32] [CHORE] change map view --- .../Map/Component/MapRepresentableView.swift | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/HotSpot/Sources/Presentation/Map/Component/MapRepresentableView.swift b/HotSpot/Sources/Presentation/Map/Component/MapRepresentableView.swift index fc2d635..fed0652 100644 --- a/HotSpot/Sources/Presentation/Map/Component/MapRepresentableView.swift +++ b/HotSpot/Sources/Presentation/Map/Component/MapRepresentableView.swift @@ -1,15 +1,53 @@ import SwiftUI import MapKit +import CoreLocation struct MapRepresentableView: UIViewRepresentable { var shops: [ShopModel] var region: Binding - class Coordinator: NSObject, MKMapViewDelegate { + class Coordinator: NSObject, MKMapViewDelegate, CLLocationManagerDelegate { var parent: MapRepresentableView + private let locationManager = CLLocationManager() + private var isFirstLocationUpdate = true init(parent: MapRepresentableView) { self.parent = parent + super.init() + locationManager.delegate = self + locationManager.requestWhenInUseAuthorization() + locationManager.startUpdatingLocation() + } + + func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + guard let location = locations.last, isFirstLocationUpdate else { return } + + isFirstLocationUpdate = false + let region = MKCoordinateRegion( + center: location.coordinate, + span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01) + ) + + DispatchQueue.main.async { + self.parent.region.wrappedValue = region + } + + locationManager.stopUpdatingLocation() + } + + func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { + isFirstLocationUpdate = false + let osakaCoordinate = CLLocationCoordinate2D(latitude: 34.6937, longitude: 135.5023) + let region = MKCoordinateRegion( + center: osakaCoordinate, + span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01) + ) + + DispatchQueue.main.async { + self.parent.region.wrappedValue = region + } + + locationManager.stopUpdatingLocation() } func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) { @@ -39,6 +77,7 @@ struct MapRepresentableView: UIViewRepresentable { let mapView = MKMapView() mapView.delegate = context.coordinator mapView.showsUserLocation = true + mapView.setRegion(region.wrappedValue, animated: false) mapView.register(ShopAnnotationView.self, forAnnotationViewWithReuseIdentifier: ShopAnnotationView.reuseIdentifier) return mapView } @@ -56,5 +95,11 @@ struct MapRepresentableView: UIViewRepresentable { } uiView.addAnnotations(annotations) + + // Update map region if it has changed + if uiView.region.center.latitude != region.wrappedValue.center.latitude || + uiView.region.center.longitude != region.wrappedValue.center.longitude { + uiView.setRegion(region.wrappedValue, animated: true) + } } } From 56c6aae22d78d71b58d28ee43f11bb0cf1cb1224 Mon Sep 17 00:00:00 2001 From: coby5502 Date: Mon, 21 Apr 2025 14:24:10 +0900 Subject: [PATCH 04/32] [FIX] fix message timing --- HotSpot/Sources/Presentation/Map/MapStore.swift | 10 +++++++++- HotSpot/Sources/Presentation/Map/MapView.swift | 5 +++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/HotSpot/Sources/Presentation/Map/MapStore.swift b/HotSpot/Sources/Presentation/Map/MapStore.swift index 1df722b..b9fed70 100644 --- a/HotSpot/Sources/Presentation/Map/MapStore.swift +++ b/HotSpot/Sources/Presentation/Map/MapStore.swift @@ -16,6 +16,7 @@ struct MapStore { ) var lastFetchedLocation: CLLocationCoordinate2D? = nil var error: ShopError? = nil + var shouldShowNoShopsMessage: Bool = false } enum Action { @@ -24,6 +25,7 @@ struct MapStore { case updateShops([ShopModel]) case handleError(Error) case clearError + case clearNoShopsMessage } var body: some ReducerOf { @@ -57,6 +59,7 @@ struct MapStore { case let .updateShops(shops): state.shops = shops state.visibleShops = filterVisibleShops(shops, in: state.region) + state.shouldShowNoShopsMessage = state.visibleShops.isEmpty return .none case let .handleError(error): @@ -73,6 +76,10 @@ struct MapStore { case .clearError: state.error = nil return .none + + case .clearNoShopsMessage: + state.shouldShowNoShopsMessage = false + return .none } } } @@ -116,6 +123,7 @@ extension MapStore.State { lhs.region.span.longitudeDelta == rhs.region.span.longitudeDelta && lhs.error == rhs.error && lhs.lastFetchedLocation?.latitude == rhs.lastFetchedLocation?.latitude && - lhs.lastFetchedLocation?.longitude == rhs.lastFetchedLocation?.longitude + lhs.lastFetchedLocation?.longitude == rhs.lastFetchedLocation?.longitude && + lhs.shouldShowNoShopsMessage == rhs.shouldShowNoShopsMessage } } diff --git a/HotSpot/Sources/Presentation/Map/MapView.swift b/HotSpot/Sources/Presentation/Map/MapView.swift index 5cbea0c..e3aebd8 100644 --- a/HotSpot/Sources/Presentation/Map/MapView.swift +++ b/HotSpot/Sources/Presentation/Map/MapView.swift @@ -61,12 +61,13 @@ struct MapView: View { viewStore.send(.clearError) } } - .onChange(of: viewStore.visibleShops) { shops in - if shops.isEmpty { + .onChange(of: viewStore.shouldShowNoShopsMessage) { shouldShow in + if shouldShow { coordinator?.showMessage( title: "お店が見つかりません", message: "ズームインして再度お試しください" ) + viewStore.send(.clearNoShopsMessage) } } } From 3532b6ced8e989e29675d1c36a8ff54d5e2c6120 Mon Sep 17 00:00:00 2001 From: coby5502 Date: Mon, 21 Apr 2025 14:50:20 +0900 Subject: [PATCH 05/32] [FEAT] deboucing --- .../Sources/Presentation/Map/MapStore.swift | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/HotSpot/Sources/Presentation/Map/MapStore.swift b/HotSpot/Sources/Presentation/Map/MapStore.swift index b9fed70..96a2d77 100644 --- a/HotSpot/Sources/Presentation/Map/MapStore.swift +++ b/HotSpot/Sources/Presentation/Map/MapStore.swift @@ -17,6 +17,7 @@ struct MapStore { var lastFetchedLocation: CLLocationCoordinate2D? = nil var error: ShopError? = nil var shouldShowNoShopsMessage: Bool = false + var isFetching: Bool = false } enum Action { @@ -26,6 +27,7 @@ struct MapStore { case handleError(Error) case clearError case clearNoShopsMessage + case setFetching(Bool) } var body: some ReducerOf { @@ -35,14 +37,19 @@ struct MapStore { state.region = region state.visibleShops = filterVisibleShops(state.shops, in: region) - if shouldFetchNewData(state: state, newRegion: region) { + if shouldFetchNewData(state: state, newRegion: region) && !state.isFetching { state.lastFetchedLocation = region.center - return .send(.fetchShops) + return .run { send in + try await Task.sleep(nanoseconds: 500_000_000) + await send(.fetchShops) + } + .cancellable(id: "fetch-shops", cancelInFlight: true) } return .none case .fetchShops: + state.isFetching = true return .run { [region = state.region] send in do { let useCase = ShopsUseCase(repository: shopRepository) @@ -54,6 +61,7 @@ struct MapStore { } catch { await send(.handleError(error)) } + await send(.setFetching(false)) } case let .updateShops(shops): @@ -80,6 +88,10 @@ struct MapStore { case .clearNoShopsMessage: state.shouldShowNoShopsMessage = false return .none + + case let .setFetching(isFetching): + state.isFetching = isFetching + return .none } } } @@ -124,6 +136,7 @@ extension MapStore.State { lhs.error == rhs.error && lhs.lastFetchedLocation?.latitude == rhs.lastFetchedLocation?.latitude && lhs.lastFetchedLocation?.longitude == rhs.lastFetchedLocation?.longitude && - lhs.shouldShowNoShopsMessage == rhs.shouldShowNoShopsMessage + lhs.shouldShowNoShopsMessage == rhs.shouldShowNoShopsMessage && + lhs.isFetching == rhs.isFetching } } From 352aa673abc82d4b4eb0129ca27a570aa178fd7b Mon Sep 17 00:00:00 2001 From: coby5502 Date: Mon, 21 Apr 2025 15:15:30 +0900 Subject: [PATCH 06/32] [CHORE] change map model --- .../Sources/Domain/Model/MapCoordinate.swift | 63 ++++++++ HotSpot/Sources/Domain/Model/ShopModel.swift | 18 +++ .../Map/Component/MapRepresentableView.swift | 22 +-- .../Sources/Presentation/Map/MapStore.swift | 150 ++++++++---------- 4 files changed, 154 insertions(+), 99 deletions(-) create mode 100644 HotSpot/Sources/Domain/Model/MapCoordinate.swift diff --git a/HotSpot/Sources/Domain/Model/MapCoordinate.swift b/HotSpot/Sources/Domain/Model/MapCoordinate.swift new file mode 100644 index 0000000..21a790e --- /dev/null +++ b/HotSpot/Sources/Domain/Model/MapCoordinate.swift @@ -0,0 +1,63 @@ +import Foundation +import CoreLocation +import MapKit + +struct MapCoordinate: Equatable { + let latitude: Double + let longitude: Double + + init(latitude: Double, longitude: Double) { + self.latitude = latitude + self.longitude = longitude + } + + init(coordinate: CLLocationCoordinate2D) { + self.latitude = coordinate.latitude + self.longitude = coordinate.longitude + } + + var clLocationCoordinate2D: CLLocationCoordinate2D { + CLLocationCoordinate2D(latitude: latitude, longitude: longitude) + } +} + +struct MapRegion: Equatable { + let center: MapCoordinate + let span: MapSpan + + init(center: MapCoordinate, span: MapSpan) { + self.center = center + self.span = span + } + + init(region: MKCoordinateRegion) { + self.center = MapCoordinate(coordinate: region.center) + self.span = MapSpan(span: region.span) + } + + var mkCoordinateRegion: MKCoordinateRegion { + MKCoordinateRegion( + center: center.clLocationCoordinate2D, + span: span.mkCoordinateSpan + ) + } +} + +struct MapSpan: Equatable { + let latitudeDelta: Double + let longitudeDelta: Double + + init(latitudeDelta: Double, longitudeDelta: Double) { + self.latitudeDelta = latitudeDelta + self.longitudeDelta = longitudeDelta + } + + init(span: MKCoordinateSpan) { + self.latitudeDelta = span.latitudeDelta + self.longitudeDelta = span.longitudeDelta + } + + var mkCoordinateSpan: MKCoordinateSpan { + MKCoordinateSpan(latitudeDelta: latitudeDelta, longitudeDelta: longitudeDelta) + } +} \ No newline at end of file diff --git a/HotSpot/Sources/Domain/Model/ShopModel.swift b/HotSpot/Sources/Domain/Model/ShopModel.swift index 082085f..8534323 100644 --- a/HotSpot/Sources/Domain/Model/ShopModel.swift +++ b/HotSpot/Sources/Domain/Model/ShopModel.swift @@ -10,4 +10,22 @@ struct ShopModel: Identifiable, Equatable { let access: String let openingHours: String? let genreCode: String + + var coordinate: MapCoordinate { + MapCoordinate(latitude: latitude, longitude: longitude) + } + + static func filterVisibleShops(_ shops: [ShopModel], in region: MapRegion) -> [ShopModel] { + shops.filter { shop in + let latMin = region.center.latitude - region.span.latitudeDelta / 2 + let latMax = region.center.latitude + region.span.latitudeDelta / 2 + let lonMin = region.center.longitude - region.span.longitudeDelta / 2 + let lonMax = region.center.longitude + region.span.longitudeDelta / 2 + + return shop.latitude >= latMin && + shop.latitude <= latMax && + shop.longitude >= lonMin && + shop.longitude <= lonMax + } + } } diff --git a/HotSpot/Sources/Presentation/Map/Component/MapRepresentableView.swift b/HotSpot/Sources/Presentation/Map/Component/MapRepresentableView.swift index fed0652..4ee34f6 100644 --- a/HotSpot/Sources/Presentation/Map/Component/MapRepresentableView.swift +++ b/HotSpot/Sources/Presentation/Map/Component/MapRepresentableView.swift @@ -4,7 +4,7 @@ import CoreLocation struct MapRepresentableView: UIViewRepresentable { var shops: [ShopModel] - var region: Binding + var region: Binding class Coordinator: NSObject, MKMapViewDelegate, CLLocationManagerDelegate { var parent: MapRepresentableView @@ -23,9 +23,9 @@ struct MapRepresentableView: UIViewRepresentable { guard let location = locations.last, isFirstLocationUpdate else { return } isFirstLocationUpdate = false - let region = MKCoordinateRegion( - center: location.coordinate, - span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01) + let region = MapRegion( + center: MapCoordinate(coordinate: location.coordinate), + span: MapSpan(latitudeDelta: 0.01, longitudeDelta: 0.01) ) DispatchQueue.main.async { @@ -37,10 +37,10 @@ struct MapRepresentableView: UIViewRepresentable { func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { isFirstLocationUpdate = false - let osakaCoordinate = CLLocationCoordinate2D(latitude: 34.6937, longitude: 135.5023) - let region = MKCoordinateRegion( + let osakaCoordinate = MapCoordinate(latitude: 34.6937, longitude: 135.5023) + let region = MapRegion( center: osakaCoordinate, - span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01) + span: MapSpan(latitudeDelta: 0.01, longitudeDelta: 0.01) ) DispatchQueue.main.async { @@ -52,7 +52,7 @@ struct MapRepresentableView: UIViewRepresentable { func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) { DispatchQueue.main.async { - self.parent.region.wrappedValue = mapView.region + self.parent.region.wrappedValue = MapRegion(region: mapView.region) } } @@ -77,7 +77,7 @@ struct MapRepresentableView: UIViewRepresentable { let mapView = MKMapView() mapView.delegate = context.coordinator mapView.showsUserLocation = true - mapView.setRegion(region.wrappedValue, animated: false) + mapView.setRegion(region.wrappedValue.mkCoordinateRegion, animated: false) mapView.register(ShopAnnotationView.self, forAnnotationViewWithReuseIdentifier: ShopAnnotationView.reuseIdentifier) return mapView } @@ -87,7 +87,7 @@ struct MapRepresentableView: UIViewRepresentable { let annotations = shops.map { ShopAnnotation( - coordinate: CLLocationCoordinate2D(latitude: $0.latitude, longitude: $0.longitude), + coordinate: $0.coordinate.clLocationCoordinate2D, title: $0.name, shopId: $0.id, genreCode: $0.genreCode @@ -99,7 +99,7 @@ struct MapRepresentableView: UIViewRepresentable { // Update map region if it has changed if uiView.region.center.latitude != region.wrappedValue.center.latitude || uiView.region.center.longitude != region.wrappedValue.center.longitude { - uiView.setRegion(region.wrappedValue, animated: true) + uiView.setRegion(region.wrappedValue.mkCoordinateRegion, animated: true) } } } diff --git a/HotSpot/Sources/Presentation/Map/MapStore.swift b/HotSpot/Sources/Presentation/Map/MapStore.swift index 96a2d77..88f78b6 100644 --- a/HotSpot/Sources/Presentation/Map/MapStore.swift +++ b/HotSpot/Sources/Presentation/Map/MapStore.swift @@ -3,31 +3,63 @@ import CoreLocation import ComposableArchitecture import MapKit -@Reducer -struct MapStore { +struct MapStore: Reducer { @Dependency(\.shopRepository) var shopRepository struct State: Equatable { var shops: [ShopModel] = [] var visibleShops: [ShopModel] = [] - var region: MKCoordinateRegion = MKCoordinateRegion( - center: CLLocationCoordinate2D(latitude: 35.6762, longitude: 139.6503), - span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01) + var region: MapRegion = MapRegion( + center: MapCoordinate(latitude: 35.6762, longitude: 139.6503), + span: MapSpan(latitudeDelta: 0.01, longitudeDelta: 0.01) ) - var lastFetchedLocation: CLLocationCoordinate2D? = nil + var lastFetchedLocation: MapCoordinate? = nil var error: ShopError? = nil var shouldShowNoShopsMessage: Bool = false - var isFetching: Bool = false + + static func == (lhs: State, rhs: State) -> Bool { + lhs.shops == rhs.shops && + lhs.visibleShops == rhs.visibleShops && + lhs.region.center.latitude == rhs.region.center.latitude && + lhs.region.center.longitude == rhs.region.center.longitude && + lhs.region.span.latitudeDelta == rhs.region.span.latitudeDelta && + lhs.region.span.longitudeDelta == rhs.region.span.longitudeDelta && + lhs.lastFetchedLocation?.latitude == rhs.lastFetchedLocation?.latitude && + lhs.lastFetchedLocation?.longitude == rhs.lastFetchedLocation?.longitude && + lhs.error == rhs.error && + lhs.shouldShowNoShopsMessage == rhs.shouldShowNoShopsMessage + } } - enum Action { - case updateRegion(MKCoordinateRegion) + enum Action: Equatable { + case updateRegion(MapRegion) case fetchShops case updateShops([ShopModel]) - case handleError(Error) + case handleError(ShopError) case clearError case clearNoShopsMessage - case setFetching(Bool) + + static func == (lhs: Action, rhs: Action) -> Bool { + switch (lhs, rhs) { + case let (.updateRegion(lRegion), .updateRegion(rRegion)): + return lRegion.center.latitude == rRegion.center.latitude && + lRegion.center.longitude == rRegion.center.longitude && + lRegion.span.latitudeDelta == rRegion.span.latitudeDelta && + lRegion.span.longitudeDelta == rRegion.span.longitudeDelta + case (.fetchShops, .fetchShops): + return true + case let (.updateShops(lShops), .updateShops(rShops)): + return lShops == rShops + case let (.handleError(lError), .handleError(rError)): + return lError == rError + case (.clearError, .clearError): + return true + case (.clearNoShopsMessage, .clearNoShopsMessage): + return true + default: + return false + } + } } var body: some ReducerOf { @@ -35,108 +67,50 @@ struct MapStore { switch action { case let .updateRegion(region): state.region = region - state.visibleShops = filterVisibleShops(state.shops, in: region) - - if shouldFetchNewData(state: state, newRegion: region) && !state.isFetching { - state.lastFetchedLocation = region.center - return .run { send in - try await Task.sleep(nanoseconds: 500_000_000) - await send(.fetchShops) - } - .cancellable(id: "fetch-shops", cancelInFlight: true) + state.visibleShops = ShopModel.filterVisibleShops(state.shops, in: region) + state.lastFetchedLocation = region.center + return .run { send in + try await Task.sleep(nanoseconds: 500_000_000) + await send(.fetchShops) } - - return .none + .cancellable(id: CancelID.fetchShops, cancelInFlight: true) case .fetchShops: - state.isFetching = true - return .run { [region = state.region] send in + let lat = state.region.center.latitude + let lng = state.region.center.longitude + + return .run { send in do { let useCase = ShopsUseCase(repository: shopRepository) - let shops = try await useCase.execute( - lat: region.center.latitude, - lng: region.center.longitude - ) + let shops = try await useCase.execute(lat: lat, lng: lng) await send(.updateShops(shops)) - } catch { + } catch let error as ShopError { await send(.handleError(error)) + } catch { + await send(.handleError(.server(message: error.localizedDescription))) } - await send(.setFetching(false)) } case let .updateShops(shops): state.shops = shops - state.visibleShops = filterVisibleShops(shops, in: state.region) + state.visibleShops = ShopModel.filterVisibleShops(shops, in: state.region) state.shouldShowNoShopsMessage = state.visibleShops.isEmpty return .none case let .handleError(error): - switch error { - case is URLError: - state.error = .network - case is DecodingError: - state.error = .decoding - default: - state.error = .server(message: error.localizedDescription) - } + state.error = error return .none - + case .clearError: state.error = nil return .none - + case .clearNoShopsMessage: state.shouldShowNoShopsMessage = false return .none - - case let .setFetching(isFetching): - state.isFetching = isFetching - return .none } } } - // MARK: - Helpers - func shouldFetchNewData(state: State, newRegion: MKCoordinateRegion) -> Bool { - guard let lastLocation = state.lastFetchedLocation else { - return true - } - - let distance = CLLocation(latitude: lastLocation.latitude, longitude: lastLocation.longitude) - .distance(from: CLLocation(latitude: newRegion.center.latitude, longitude: newRegion.center.longitude)) - - return distance > 100 - } - - func filterVisibleShops(_ shops: [ShopModel], in region: MKCoordinateRegion) -> [ShopModel] { - shops.filter { shop in - let coordinate = CLLocationCoordinate2D(latitude: shop.latitude, longitude: shop.longitude) - let latMin = region.center.latitude - region.span.latitudeDelta / 2 - let latMax = region.center.latitude + region.span.latitudeDelta / 2 - let lonMin = region.center.longitude - region.span.longitudeDelta / 2 - let lonMax = region.center.longitude + region.span.longitudeDelta / 2 - - return coordinate.latitude >= latMin && - coordinate.latitude <= latMax && - coordinate.longitude >= lonMin && - coordinate.longitude <= lonMax - } - } -} - -// MARK: - Equatable -extension MapStore.State { - static func == (lhs: MapStore.State, rhs: MapStore.State) -> Bool { - lhs.shops == rhs.shops && - lhs.visibleShops == rhs.visibleShops && - lhs.region.center.latitude == rhs.region.center.latitude && - lhs.region.center.longitude == rhs.region.center.longitude && - lhs.region.span.latitudeDelta == rhs.region.span.latitudeDelta && - lhs.region.span.longitudeDelta == rhs.region.span.longitudeDelta && - lhs.error == rhs.error && - lhs.lastFetchedLocation?.latitude == rhs.lastFetchedLocation?.latitude && - lhs.lastFetchedLocation?.longitude == rhs.lastFetchedLocation?.longitude && - lhs.shouldShowNoShopsMessage == rhs.shouldShowNoShopsMessage && - lhs.isFetching == rhs.isFetching - } + enum CancelID { case fetchShops } } From f64cac6dd920eaa991a5cd3cf690708d748ea4c6 Mon Sep 17 00:00:00 2001 From: coby5502 Date: Mon, 21 Apr 2025 15:20:02 +0900 Subject: [PATCH 07/32] [CHORE] delete equtable --- .../Sources/Presentation/Map/MapStore.swift | 35 ------------------- .../Presentation/Search/SearchStore.swift | 21 +++-------- 2 files changed, 5 insertions(+), 51 deletions(-) diff --git a/HotSpot/Sources/Presentation/Map/MapStore.swift b/HotSpot/Sources/Presentation/Map/MapStore.swift index 88f78b6..c08e050 100644 --- a/HotSpot/Sources/Presentation/Map/MapStore.swift +++ b/HotSpot/Sources/Presentation/Map/MapStore.swift @@ -16,19 +16,6 @@ struct MapStore: Reducer { var lastFetchedLocation: MapCoordinate? = nil var error: ShopError? = nil var shouldShowNoShopsMessage: Bool = false - - static func == (lhs: State, rhs: State) -> Bool { - lhs.shops == rhs.shops && - lhs.visibleShops == rhs.visibleShops && - lhs.region.center.latitude == rhs.region.center.latitude && - lhs.region.center.longitude == rhs.region.center.longitude && - lhs.region.span.latitudeDelta == rhs.region.span.latitudeDelta && - lhs.region.span.longitudeDelta == rhs.region.span.longitudeDelta && - lhs.lastFetchedLocation?.latitude == rhs.lastFetchedLocation?.latitude && - lhs.lastFetchedLocation?.longitude == rhs.lastFetchedLocation?.longitude && - lhs.error == rhs.error && - lhs.shouldShowNoShopsMessage == rhs.shouldShowNoShopsMessage - } } enum Action: Equatable { @@ -38,28 +25,6 @@ struct MapStore: Reducer { case handleError(ShopError) case clearError case clearNoShopsMessage - - static func == (lhs: Action, rhs: Action) -> Bool { - switch (lhs, rhs) { - case let (.updateRegion(lRegion), .updateRegion(rRegion)): - return lRegion.center.latitude == rRegion.center.latitude && - lRegion.center.longitude == rRegion.center.longitude && - lRegion.span.latitudeDelta == rRegion.span.latitudeDelta && - lRegion.span.longitudeDelta == rRegion.span.longitudeDelta - case (.fetchShops, .fetchShops): - return true - case let (.updateShops(lShops), .updateShops(rShops)): - return lShops == rShops - case let (.handleError(lError), .handleError(rError)): - return lError == rError - case (.clearError, .clearError): - return true - case (.clearNoShopsMessage, .clearNoShopsMessage): - return true - default: - return false - } - } } var body: some ReducerOf { diff --git a/HotSpot/Sources/Presentation/Search/SearchStore.swift b/HotSpot/Sources/Presentation/Search/SearchStore.swift index d1e39af..cc50953 100644 --- a/HotSpot/Sources/Presentation/Search/SearchStore.swift +++ b/HotSpot/Sources/Presentation/Search/SearchStore.swift @@ -11,26 +11,15 @@ struct SearchStore { var shops: [ShopModel] = [] var searchText: String = "" var error: ShopError? = nil - var currentLocation: CLLocationCoordinate2D? + var currentLocation: MapCoordinate? var paginationState: PaginationState = .init() - var filterState: SearchFilterStore.State = .init() - - static func == (lhs: State, rhs: State) -> Bool { - lhs.shops == rhs.shops && - lhs.searchText == rhs.searchText && - lhs.error == rhs.error && - lhs.currentLocation?.latitude == rhs.currentLocation?.latitude && - lhs.currentLocation?.longitude == rhs.currentLocation?.longitude && - lhs.paginationState == rhs.paginationState && - lhs.filterState == rhs.filterState - } } enum Action { case onAppear case search(String) - case updateLocation(CLLocationCoordinate2D) + case updateLocation(MapCoordinate) case updateShops([ShopModel]) case handleError(Error) case clearError @@ -45,7 +34,7 @@ struct SearchStore { case .onAppear: return .run { send in if let location = await locationManager.requestLocation() { - await send(.updateLocation(location.coordinate)) + await send(.updateLocation(MapCoordinate(coordinate: location.coordinate))) } } @@ -80,7 +69,7 @@ struct SearchStore { return .run { [state] send in do { - let location = state.currentLocation ?? CLLocationCoordinate2D(latitude: 34.6937, longitude: 135.5023) + let location = state.currentLocation ?? MapCoordinate(latitude: 34.6937, longitude: 135.5023) let request = ShopSearchRequestDTO( lat: location.latitude, lng: location.longitude, @@ -118,7 +107,7 @@ struct SearchStore { case .loadMore: return .run { [state] send in do { - let location = state.currentLocation ?? CLLocationCoordinate2D(latitude: 34.6937, longitude: 135.5023) + let location = state.currentLocation ?? MapCoordinate(latitude: 34.6937, longitude: 135.5023) let request = ShopSearchRequestDTO( lat: location.latitude, lng: location.longitude, From 7c55366b6f9b86c765fce18af0b7ca8c1ae04c84 Mon Sep 17 00:00:00 2001 From: coby5502 Date: Mon, 21 Apr 2025 15:41:07 +0900 Subject: [PATCH 08/32] [CHORE] fix MapStore --- HotSpot/Sources/Presentation/Map/MapStore.swift | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/HotSpot/Sources/Presentation/Map/MapStore.swift b/HotSpot/Sources/Presentation/Map/MapStore.swift index c08e050..c0ff7d8 100644 --- a/HotSpot/Sources/Presentation/Map/MapStore.swift +++ b/HotSpot/Sources/Presentation/Map/MapStore.swift @@ -30,15 +30,12 @@ struct MapStore: Reducer { var body: some ReducerOf { Reduce { state, action in switch action { + case let .updateRegion(region): state.region = region state.visibleShops = ShopModel.filterVisibleShops(state.shops, in: region) state.lastFetchedLocation = region.center - return .run { send in - try await Task.sleep(nanoseconds: 500_000_000) - await send(.fetchShops) - } - .cancellable(id: CancelID.fetchShops, cancelInFlight: true) + return .send(.fetchShops) case .fetchShops: let lat = state.region.center.latitude @@ -76,6 +73,4 @@ struct MapStore: Reducer { } } } - - enum CancelID { case fetchShops } } From 4e60ff8f388bf67b432af8a30840a32086fc90a4 Mon Sep 17 00:00:00 2001 From: coby5502 Date: Mon, 21 Apr 2025 17:41:44 +0900 Subject: [PATCH 09/32] [CHORE] fix message logic --- HotSpot/Sources/Presentation/Map/MapStore.swift | 10 +++------- HotSpot/Sources/Presentation/Map/MapView.swift | 5 ++--- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/HotSpot/Sources/Presentation/Map/MapStore.swift b/HotSpot/Sources/Presentation/Map/MapStore.swift index c0ff7d8..7ecc8aa 100644 --- a/HotSpot/Sources/Presentation/Map/MapStore.swift +++ b/HotSpot/Sources/Presentation/Map/MapStore.swift @@ -15,7 +15,6 @@ struct MapStore: Reducer { ) var lastFetchedLocation: MapCoordinate? = nil var error: ShopError? = nil - var shouldShowNoShopsMessage: Bool = false } enum Action: Equatable { @@ -24,9 +23,10 @@ struct MapStore: Reducer { case updateShops([ShopModel]) case handleError(ShopError) case clearError - case clearNoShopsMessage } + enum CancelID { case fetchShops } + var body: some ReducerOf { Reduce { state, action in switch action { @@ -52,11 +52,11 @@ struct MapStore: Reducer { await send(.handleError(.server(message: error.localizedDescription))) } } + .cancellable(id: CancelID.fetchShops, cancelInFlight: true) case let .updateShops(shops): state.shops = shops state.visibleShops = ShopModel.filterVisibleShops(shops, in: state.region) - state.shouldShowNoShopsMessage = state.visibleShops.isEmpty return .none case let .handleError(error): @@ -66,10 +66,6 @@ struct MapStore: Reducer { case .clearError: state.error = nil return .none - - case .clearNoShopsMessage: - state.shouldShowNoShopsMessage = false - return .none } } } diff --git a/HotSpot/Sources/Presentation/Map/MapView.swift b/HotSpot/Sources/Presentation/Map/MapView.swift index e3aebd8..5cbea0c 100644 --- a/HotSpot/Sources/Presentation/Map/MapView.swift +++ b/HotSpot/Sources/Presentation/Map/MapView.swift @@ -61,13 +61,12 @@ struct MapView: View { viewStore.send(.clearError) } } - .onChange(of: viewStore.shouldShowNoShopsMessage) { shouldShow in - if shouldShow { + .onChange(of: viewStore.visibleShops) { shops in + if shops.isEmpty { coordinator?.showMessage( title: "お店が見つかりません", message: "ズームインして再度お試しください" ) - viewStore.send(.clearNoShopsMessage) } } } From 7b423b7551c987b5d608aa0ab7f0744fedf7ba9e Mon Sep 17 00:00:00 2001 From: coby5502 Date: Mon, 21 Apr 2025 19:34:02 +0900 Subject: [PATCH 10/32] [FEAT] mapview carousel --- .../Map/Component/SnappingScrollView.swift | 164 +++++++++++------- .../Sources/Presentation/Map/MapView.swift | 9 +- 2 files changed, 109 insertions(+), 64 deletions(-) diff --git a/HotSpot/Sources/Presentation/Map/Component/SnappingScrollView.swift b/HotSpot/Sources/Presentation/Map/Component/SnappingScrollView.swift index 9fd318e..991ecdd 100644 --- a/HotSpot/Sources/Presentation/Map/Component/SnappingScrollView.swift +++ b/HotSpot/Sources/Presentation/Map/Component/SnappingScrollView.swift @@ -1,68 +1,110 @@ import SwiftUI -import CobyDS - -struct SnappingScrollView: View { - let items: [Item] - let itemWidth: CGFloat - let spacing: CGFloat - let content: (Item) -> Content - - @State private var scrollOffset: CGFloat = 0 - - init( - items: [Item], - itemWidth: CGFloat, - spacing: CGFloat = 8, - @ViewBuilder content: @escaping (Item) -> Content - ) { - self.items = items - self.itemWidth = itemWidth - self.spacing = spacing - self.content = content +import UIKit + +struct CarouselScrollViewRepresentable: UIViewRepresentable { + var items: [Item] + var itemWidth: CGFloat + var spacing: CGFloat + var content: (Item) -> Content + + func makeCoordinator() -> Coordinator { + Coordinator(items: items, itemWidth: itemWidth, spacing: spacing, content: content) } - - var body: some View { - ScrollViewReader { proxy in - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: spacing) { - ForEach(items) { item in - content(item) - .id(item.id) - } - } - .padding(.horizontal, BaseSize.horizantalPadding) - .background( - GeometryReader { geometry in - Color.clear.preference( - key: ScrollOffsetPreferenceKey.self, - value: geometry.frame(in: .named("scroll")).minX - ) - } - ) - } - .coordinateSpace(name: "scroll") - .onPreferenceChange(ScrollOffsetPreferenceKey.self) { offset in - scrollOffset = offset + + func makeUIView(context: Context) -> UICollectionView { + return context.coordinator.collectionView + } + + func updateUIView(_ uiView: UICollectionView, context: Context) { + context.coordinator.update(items: items) + } + + class Coordinator: NSObject, UICollectionViewDelegateFlowLayout, UICollectionViewDataSource, UIScrollViewDelegate { + var items: [Item] + let itemWidth: CGFloat + let spacing: CGFloat + let content: (Item) -> Content + + var collectionView: UICollectionView + + init(items: [Item], itemWidth: CGFloat, spacing: CGFloat, content: @escaping (Item) -> Content) { + self.items = items + self.itemWidth = itemWidth + self.spacing = spacing + self.content = content + + let layout = UICollectionViewFlowLayout() + layout.scrollDirection = .horizontal + layout.minimumLineSpacing = spacing + layout.sectionInset = UIEdgeInsets(top: 0, left: (UIScreen.main.bounds.width - itemWidth) / 2, bottom: 0, right: (UIScreen.main.bounds.width - itemWidth) / 2) + + collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) + collectionView.showsHorizontalScrollIndicator = false + collectionView.decelerationRate = .fast + collectionView.backgroundColor = .clear + + super.init() + + collectionView.delegate = self + collectionView.dataSource = self + collectionView.register(CarouselCell.self, forCellWithReuseIdentifier: "CarouselCell") + } + + func update(items: [Item]) { + self.items = items + collectionView.reloadData() + } + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + items.count + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + CGSize(width: itemWidth, height: collectionView.frame.height) + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CarouselCell", for: indexPath) as? CarouselCell else { + return UICollectionViewCell() } - .gesture( - DragGesture() - .onEnded { _ in - let totalWidth = itemWidth + spacing - let currentIndex = Int(round(scrollOffset / totalWidth)) - let adjustedIndex = max(0, min(currentIndex, items.count - 1)) - - withAnimation(.easeOut(duration: 0.2)) { - proxy.scrollTo(items[adjustedIndex].id, anchor: .center) - } - } - ) + + let hostingController = UIHostingController(rootView: content(items[indexPath.item])) + hostingController.view.translatesAutoresizingMaskIntoConstraints = false + hostingController.view.backgroundColor = .clear + + cell.hostingController = hostingController + return cell + } + + func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { + let layout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout + let cellWidthIncludingSpacing = itemWidth + spacing + + let offsetX = targetContentOffset.pointee.x + let index = round((offsetX + scrollView.contentInset.left) / cellWidthIncludingSpacing) + let newOffsetX = index * cellWidthIncludingSpacing - scrollView.contentInset.left + + targetContentOffset.pointee = CGPoint(x: newOffsetX, y: 0) } } -} -struct ScrollOffsetPreferenceKey: PreferenceKey { - static var defaultValue: CGFloat = 0 - static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { - value = nextValue() + class CarouselCell: UICollectionViewCell { + var hostingController: UIHostingController? { + didSet { + if let oldVC = oldValue { + oldVC.view.removeFromSuperview() + } + + if let vc = hostingController { + contentView.addSubview(vc.view) + NSLayoutConstraint.activate([ + vc.view.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + vc.view.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + vc.view.topAnchor.constraint(equalTo: contentView.topAnchor), + vc.view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) + ]) + } + } + } } -} +} diff --git a/HotSpot/Sources/Presentation/Map/MapView.swift b/HotSpot/Sources/Presentation/Map/MapView.swift index 5cbea0c..e840f68 100644 --- a/HotSpot/Sources/Presentation/Map/MapView.swift +++ b/HotSpot/Sources/Presentation/Map/MapView.swift @@ -33,9 +33,10 @@ struct MapView: View { .ignoresSafeArea(.all, edges: .bottom) // Bottom card scroll view - SnappingScrollView( + CarouselScrollViewRepresentable( items: viewStore.visibleShops, - itemWidth: BaseSize.fullWidth + itemWidth: BaseSize.fullWidth, + spacing: 8 ) { shop in ThumbnailTileView( image: shopImages[shop.id], @@ -44,7 +45,6 @@ struct MapView: View { description: shop.access, subDescription: nil ) - .frame(width: BaseSize.fullWidth) .onTapGesture { coordinator?.showShopDetail(shop) } @@ -52,6 +52,9 @@ struct MapView: View { loadImage(for: shop) } } + .frame(maxWidth: .infinity) + .frame(height: 120) + .background(Color.red) .padding(.bottom, 30) } } From 5d002154ed01d9ba5cbeda0c861933fd7722a036 Mon Sep 17 00:00:00 2001 From: coby5502 Date: Mon, 21 Apr 2025 19:59:34 +0900 Subject: [PATCH 11/32] [CHORE] Complete ShopDetailView Design --- .../Extensions/UIImage+Kingfisher.swift | 2 +- ... => CarouselScrollViewRepresentable.swift} | 0 .../Sources/Presentation/Map/MapView.swift | 44 ++++----- .../Component/ShopInfoSection.swift | 34 ++++--- .../Component/ShopLocationMapView.swift | 91 +++++++++---------- 5 files changed, 88 insertions(+), 83 deletions(-) rename HotSpot/Sources/Presentation/Map/Component/{SnappingScrollView.swift => CarouselScrollViewRepresentable.swift} (100%) diff --git a/HotSpot/Sources/Common/Extensions/UIImage+Kingfisher.swift b/HotSpot/Sources/Common/Extensions/UIImage+Kingfisher.swift index 6eded86..945e83a 100644 --- a/HotSpot/Sources/Common/Extensions/UIImage+Kingfisher.swift +++ b/HotSpot/Sources/Common/Extensions/UIImage+Kingfisher.swift @@ -26,7 +26,7 @@ extension UIImage { return } - let processor = DownsamplingImageProcessor(size: CGSize(width: 200, height: 200)) + let processor = DownsamplingImageProcessor(size: CGSize(width: 100, height: 100)) KingfisherManager.shared.retrieveImage( with: url, options: [ diff --git a/HotSpot/Sources/Presentation/Map/Component/SnappingScrollView.swift b/HotSpot/Sources/Presentation/Map/Component/CarouselScrollViewRepresentable.swift similarity index 100% rename from HotSpot/Sources/Presentation/Map/Component/SnappingScrollView.swift rename to HotSpot/Sources/Presentation/Map/Component/CarouselScrollViewRepresentable.swift diff --git a/HotSpot/Sources/Presentation/Map/MapView.swift b/HotSpot/Sources/Presentation/Map/MapView.swift index e840f68..1e2c017 100644 --- a/HotSpot/Sources/Presentation/Map/MapView.swift +++ b/HotSpot/Sources/Presentation/Map/MapView.swift @@ -32,30 +32,30 @@ struct MapView: View { ) .ignoresSafeArea(.all, edges: .bottom) - // Bottom card scroll view - CarouselScrollViewRepresentable( - items: viewStore.visibleShops, - itemWidth: BaseSize.fullWidth, - spacing: 8 - ) { shop in - ThumbnailTileView( - image: shopImages[shop.id], - title: shop.name, - subTitle: nil, - description: shop.access, - subDescription: nil - ) - .onTapGesture { - coordinator?.showShopDetail(shop) - } - .onAppear { - loadImage(for: shop) + if !viewStore.visibleShops.isEmpty { + CarouselScrollViewRepresentable( + items: viewStore.visibleShops, + itemWidth: BaseSize.fullWidth, + spacing: 8 + ) { shop in + ThumbnailTileView( + image: shopImages[shop.id], + title: shop.name, + subTitle: nil, + description: shop.access, + subDescription: nil + ) + .onTapGesture { + coordinator?.showShopDetail(shop) + } + .onAppear { + loadImage(for: shop) + } } + .frame(maxWidth: .infinity) + .frame(height: 120) + .padding(.bottom, 30) } - .frame(maxWidth: .infinity) - .frame(height: 120) - .background(Color.red) - .padding(.bottom, 30) } } .onChange(of: viewStore.error) { error in diff --git a/HotSpot/Sources/Presentation/ShopDetail/Component/ShopInfoSection.swift b/HotSpot/Sources/Presentation/ShopDetail/Component/ShopInfoSection.swift index 0f4806b..f6db786 100644 --- a/HotSpot/Sources/Presentation/ShopDetail/Component/ShopInfoSection.swift +++ b/HotSpot/Sources/Presentation/ShopDetail/Component/ShopInfoSection.swift @@ -10,44 +10,54 @@ struct ShopInfoSection: View { // Name and Genre VStack(alignment: .leading, spacing: 8) { Text(shop.name) - .font(.title) - .fontWeight(.bold) + .font(.pretendard(size: 24, weight: .bold)) + .foregroundColor(Color.labelNormal) Text(ShopGenre.name(for: shop.genreCode)) - .font(.subheadline) - .foregroundColor(.gray) + .font(.pretendard(size: 14, weight: .regular)) + .foregroundColor(Color.labelAlternative) } // Address VStack(alignment: .leading, spacing: 4) { Text("住所") - .font(.headline) + .font(.pretendard(size: 16, weight: .semibold)) + .foregroundColor(Color.labelNormal) + Text(shop.address) - .font(.body) + .font(.pretendard(size: 16, weight: .regular)) + .foregroundColor(Color.labelNeutral) } // Access VStack(alignment: .leading, spacing: 4) { Text("アクセス") - .font(.headline) + .font(.pretendard(size: 16, weight: .semibold)) + .foregroundColor(Color.labelNormal) + Text(shop.access) - .font(.body) + .font(.pretendard(size: 16, weight: .regular)) + .foregroundColor(Color.labelNeutral) } // Open Hours if let openingHours = shop.openingHours { VStack(alignment: .leading, spacing: 4) { Text("営業時間") - .font(.headline) + .font(.pretendard(size: 16, weight: .semibold)) + .foregroundColor(Color.labelNormal) + Text(openingHours) - .font(.body) + .font(.pretendard(size: 16, weight: .regular)) + .foregroundColor(Color.labelNeutral) } } // Location - VStack(alignment: .leading, spacing: 4) { + VStack(alignment: .leading, spacing: 8) { Text("位置情報") - .font(.headline) + .font(.pretendard(size: 16, weight: .semibold)) + .foregroundColor(Color.labelNormal) ShopLocationMapView(shop: shop) } diff --git a/HotSpot/Sources/Presentation/ShopDetail/Component/ShopLocationMapView.swift b/HotSpot/Sources/Presentation/ShopDetail/Component/ShopLocationMapView.swift index eb020c5..46e7638 100644 --- a/HotSpot/Sources/Presentation/ShopDetail/Component/ShopLocationMapView.swift +++ b/HotSpot/Sources/Presentation/ShopDetail/Component/ShopLocationMapView.swift @@ -3,57 +3,52 @@ import MapKit struct ShopLocationMapView: View { let shop: ShopModel - + var body: some View { - GeometryReader { geometry in - let coordinate = CLLocationCoordinate2D( - latitude: shop.latitude, - longitude: shop.longitude - ) - - let region = MKCoordinateRegion( - center: coordinate, - span: MKCoordinateSpan( - latitudeDelta: 0.01, - longitudeDelta: 0.01 - ) - ) - - Map( - coordinateRegion: .constant(region), - interactionModes: [] - ) - .frame(height: 200) - .cornerRadius(8) - .overlay( - Image(systemName: "mappin.circle.fill") - .resizable() - .frame(width: 32, height: 32) - .foregroundColor(Color(uiColor: ShopGenre.color(for: shop.genreCode))) - ) - .onTapGesture { - let placemark = MKPlacemark(coordinate: coordinate) - let mapItem = MKMapItem(placemark: placemark) - mapItem.name = shop.name - mapItem.openInMaps() + let coordinate = CLLocationCoordinate2D( + latitude: shop.latitude, + longitude: shop.longitude + ) + + let region = MKCoordinateRegion( + center: coordinate, + span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01) + ) + + Map(coordinateRegion: .constant(region), annotationItems: [ShopPin(shop: shop)]) { pin in + MapAnnotation(coordinate: pin.coordinate) { + ZStack { + Circle() + .fill(Color(uiColor: ShopGenre.color(for: pin.genreCode))) + .frame(width: 40, height: 40) + + Image(uiImage: ShopGenre.image(for: pin.genreCode) ?? UIImage()) + .resizable() + .renderingMode(.template) + .foregroundColor(.white) + .scaledToFit() + .frame(width: 24, height: 24) + } } } - .frame(height: 200) + .frame(height: 300) + .cornerRadius(8) + .onTapGesture { + let placemark = MKPlacemark(coordinate: coordinate) + let mapItem = MKMapItem(placemark: placemark) + mapItem.name = shop.name + mapItem.openInMaps() + } } } -#Preview { - ShopLocationMapView( - shop: ShopModel( - id: "test", - name: "テスト店舗", - address: "東京都渋谷区", - latitude: 35.6762, - longitude: 139.6503, - imageUrl: "https://example.com/image.jpg", - access: "渋谷駅から徒歩5分", - openingHours: "11:00-23:00", - genreCode: "G001" - ) - ) -} \ No newline at end of file +private struct ShopPin: Identifiable { + let id = UUID() + let coordinate: CLLocationCoordinate2D + let genreCode: String + + init(shop: ShopModel) { + self.coordinate = CLLocationCoordinate2D(latitude: shop.latitude, longitude: shop.longitude) + self.genreCode = shop.genreCode + } +} From 10644dec480bfb6973b7919ba0e5c42e66584ebb Mon Sep 17 00:00:00 2001 From: coby5502 Date: Mon, 21 Apr 2025 20:08:53 +0900 Subject: [PATCH 12/32] [FEAT] set index --- .../CarouselScrollViewRepresentable.swift | 47 ++++++++++++------- .../Sources/Presentation/Map/MapView.swift | 6 ++- 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/HotSpot/Sources/Presentation/Map/Component/CarouselScrollViewRepresentable.swift b/HotSpot/Sources/Presentation/Map/Component/CarouselScrollViewRepresentable.swift index 991ecdd..d7689b4 100644 --- a/HotSpot/Sources/Presentation/Map/Component/CarouselScrollViewRepresentable.swift +++ b/HotSpot/Sources/Presentation/Map/Component/CarouselScrollViewRepresentable.swift @@ -5,18 +5,25 @@ struct CarouselScrollViewRepresentable: UIVie var items: [Item] var itemWidth: CGFloat var spacing: CGFloat + @Binding var currentIndex: Int var content: (Item) -> Content func makeCoordinator() -> Coordinator { - Coordinator(items: items, itemWidth: itemWidth, spacing: spacing, content: content) + Coordinator(items: items, itemWidth: itemWidth, spacing: spacing, currentIndex: $currentIndex, content: content) } func makeUIView(context: Context) -> UICollectionView { - return context.coordinator.collectionView + context.coordinator.collectionView } func updateUIView(_ uiView: UICollectionView, context: Context) { context.coordinator.update(items: items) + + if context.coordinator.currentVisibleIndex != currentIndex { + let indexPath = IndexPath(item: currentIndex, section: 0) + context.coordinator.collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true) + context.coordinator.currentVisibleIndex = currentIndex + } } class Coordinator: NSObject, UICollectionViewDelegateFlowLayout, UICollectionViewDataSource, UIScrollViewDelegate { @@ -24,24 +31,33 @@ struct CarouselScrollViewRepresentable: UIVie let itemWidth: CGFloat let spacing: CGFloat let content: (Item) -> Content + var currentVisibleIndex: Int + var currentIndex: Binding var collectionView: UICollectionView - init(items: [Item], itemWidth: CGFloat, spacing: CGFloat, content: @escaping (Item) -> Content) { + init(items: [Item], itemWidth: CGFloat, spacing: CGFloat, currentIndex: Binding, content: @escaping (Item) -> Content) { self.items = items self.itemWidth = itemWidth self.spacing = spacing self.content = content + self.currentIndex = currentIndex + self.currentVisibleIndex = currentIndex.wrappedValue let layout = UICollectionViewFlowLayout() layout.scrollDirection = .horizontal layout.minimumLineSpacing = spacing - layout.sectionInset = UIEdgeInsets(top: 0, left: (UIScreen.main.bounds.width - itemWidth) / 2, bottom: 0, right: (UIScreen.main.bounds.width - itemWidth) / 2) - - collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) - collectionView.showsHorizontalScrollIndicator = false - collectionView.decelerationRate = .fast - collectionView.backgroundColor = .clear + layout.sectionInset = UIEdgeInsets( + top: 0, + left: (UIScreen.main.bounds.width - itemWidth) / 2, + bottom: 0, + right: (UIScreen.main.bounds.width - itemWidth) / 2 + ) + + self.collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) + self.collectionView.showsHorizontalScrollIndicator = false + self.collectionView.decelerationRate = .fast + self.collectionView.backgroundColor = .clear super.init() @@ -80,21 +96,20 @@ struct CarouselScrollViewRepresentable: UIVie let layout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout let cellWidthIncludingSpacing = itemWidth + spacing - let offsetX = targetContentOffset.pointee.x - let index = round((offsetX + scrollView.contentInset.left) / cellWidthIncludingSpacing) - let newOffsetX = index * cellWidthIncludingSpacing - scrollView.contentInset.left + let offsetX = targetContentOffset.pointee.x + scrollView.contentInset.left + let index = Int(round(offsetX / cellWidthIncludingSpacing)) + let newOffsetX = CGFloat(index) * cellWidthIncludingSpacing - scrollView.contentInset.left targetContentOffset.pointee = CGPoint(x: newOffsetX, y: 0) + currentIndex.wrappedValue = index + currentVisibleIndex = index } } class CarouselCell: UICollectionViewCell { var hostingController: UIHostingController? { didSet { - if let oldVC = oldValue { - oldVC.view.removeFromSuperview() - } - + oldValue?.view.removeFromSuperview() if let vc = hostingController { contentView.addSubview(vc.view) NSLayoutConstraint.activate([ diff --git a/HotSpot/Sources/Presentation/Map/MapView.swift b/HotSpot/Sources/Presentation/Map/MapView.swift index 1e2c017..bc2b194 100644 --- a/HotSpot/Sources/Presentation/Map/MapView.swift +++ b/HotSpot/Sources/Presentation/Map/MapView.swift @@ -7,6 +7,7 @@ import Kingfisher struct MapView: View { let store: StoreOf @State private var shopImages: [String: UIImage] = [:] + @State private var currentIndex: Int = 0 @Environment(\.coordinator) private var coordinator var body: some View { @@ -36,7 +37,8 @@ struct MapView: View { CarouselScrollViewRepresentable( items: viewStore.visibleShops, itemWidth: BaseSize.fullWidth, - spacing: 8 + spacing: 8, + currentIndex: $currentIndex ) { shop in ThumbnailTileView( image: shopImages[shop.id], @@ -71,6 +73,8 @@ struct MapView: View { message: "ズームインして再度お試しください" ) } + + currentIndex = 0 } } } From 1f25548f3cabdf193761f0822c2317c77293c162 Mon Sep 17 00:00:00 2001 From: coby5502 Date: Mon, 21 Apr 2025 20:23:18 +0900 Subject: [PATCH 13/32] [FEAT] marker focusing --- .../Map/Component/MapRepresentableView.swift | 16 ++++++++++++++++ .../Map/Component/ShopAnnotationView.swift | 10 ++++++++-- HotSpot/Sources/Presentation/Map/MapView.swift | 5 ++++- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/HotSpot/Sources/Presentation/Map/Component/MapRepresentableView.swift b/HotSpot/Sources/Presentation/Map/Component/MapRepresentableView.swift index 4ee34f6..ff865c3 100644 --- a/HotSpot/Sources/Presentation/Map/Component/MapRepresentableView.swift +++ b/HotSpot/Sources/Presentation/Map/Component/MapRepresentableView.swift @@ -5,6 +5,7 @@ import CoreLocation struct MapRepresentableView: UIViewRepresentable { var shops: [ShopModel] var region: Binding + var onMarkerSelected: ((Int) -> Void)? class Coordinator: NSObject, MKMapViewDelegate, CLLocationManagerDelegate { var parent: MapRepresentableView @@ -19,6 +20,10 @@ struct MapRepresentableView: UIViewRepresentable { locationManager.startUpdatingLocation() } + func updateShops(_ newShops: [ShopModel]) { + parent.shops = newShops + } + func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { guard let location = locations.last, isFirstLocationUpdate else { return } @@ -67,6 +72,15 @@ struct MapRepresentableView: UIViewRepresentable { return nil } + + func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) { + if let shopAnnotation = view.annotation as? ShopAnnotation { + if let index = parent.shops.firstIndex(where: { $0.id == shopAnnotation.shopId }) { + parent.onMarkerSelected?(index) + } + } + mapView.deselectAnnotation(view.annotation, animated: false) + } } func makeCoordinator() -> Coordinator { @@ -83,6 +97,8 @@ struct MapRepresentableView: UIViewRepresentable { } func updateUIView(_ uiView: MKMapView, context: Context) { + context.coordinator.updateShops(shops) + uiView.removeAnnotations(uiView.annotations) let annotations = shops.map { diff --git a/HotSpot/Sources/Presentation/Map/Component/ShopAnnotationView.swift b/HotSpot/Sources/Presentation/Map/Component/ShopAnnotationView.swift index 02ce185..e11cbd2 100644 --- a/HotSpot/Sources/Presentation/Map/Component/ShopAnnotationView.swift +++ b/HotSpot/Sources/Presentation/Map/Component/ShopAnnotationView.swift @@ -9,9 +9,11 @@ class ShopAnnotationView: MKMarkerAnnotationView { guard let shopAnnotation = newValue as? ShopAnnotation else { return } clusteringIdentifier = "Shop" canShowCallout = false - isEnabled = false + isEnabled = true markerTintColor = ShopGenre.color(for: shopAnnotation.genreCode) - glyphImage = ShopGenre.image(for: shopAnnotation.genreCode) + if let originalImage = ShopGenre.image(for: shopAnnotation.genreCode) { + glyphImage = originalImage.withTintColor(.white, renderingMode: .alwaysTemplate) + } } } @@ -29,4 +31,8 @@ class ShopAnnotationView: MKMarkerAnnotationView { frame = CGRect(x: 0, y: 0, width: 40, height: 40) centerOffset = CGPoint(x: 0, y: -frame.size.height / 2) } + + override func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(false, animated: false) + } } diff --git a/HotSpot/Sources/Presentation/Map/MapView.swift b/HotSpot/Sources/Presentation/Map/MapView.swift index bc2b194..b800919 100644 --- a/HotSpot/Sources/Presentation/Map/MapView.swift +++ b/HotSpot/Sources/Presentation/Map/MapView.swift @@ -29,7 +29,10 @@ struct MapView: View { region: viewStore.binding( get: { $0.region }, send: { .updateRegion($0) } - ) + ), + onMarkerSelected: { index in + currentIndex = index + } ) .ignoresSafeArea(.all, edges: .bottom) From 57391f6f4b5f9c6c47bf9ec535d90153a7851937 Mon Sep 17 00:00:00 2001 From: coby5502 Date: Mon, 21 Apr 2025 20:44:25 +0900 Subject: [PATCH 14/32] [FIX] image load --- .../Map/Component/CarouselScrollViewRepresentable.swift | 1 - HotSpot/Sources/Presentation/Map/MapView.swift | 2 +- .../Sources/Presentation/Search/Component/SearchResults.swift | 2 +- Tuist/Package.resolved | 4 ++-- Tuist/Package.swift | 2 +- 5 files changed, 5 insertions(+), 6 deletions(-) diff --git a/HotSpot/Sources/Presentation/Map/Component/CarouselScrollViewRepresentable.swift b/HotSpot/Sources/Presentation/Map/Component/CarouselScrollViewRepresentable.swift index d7689b4..c60b51b 100644 --- a/HotSpot/Sources/Presentation/Map/Component/CarouselScrollViewRepresentable.swift +++ b/HotSpot/Sources/Presentation/Map/Component/CarouselScrollViewRepresentable.swift @@ -93,7 +93,6 @@ struct CarouselScrollViewRepresentable: UIVie } func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { - let layout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout let cellWidthIncludingSpacing = itemWidth + spacing let offsetX = targetContentOffset.pointee.x + scrollView.contentInset.left diff --git a/HotSpot/Sources/Presentation/Map/MapView.swift b/HotSpot/Sources/Presentation/Map/MapView.swift index b800919..bad31f2 100644 --- a/HotSpot/Sources/Presentation/Map/MapView.swift +++ b/HotSpot/Sources/Presentation/Map/MapView.swift @@ -44,7 +44,7 @@ struct MapView: View { currentIndex: $currentIndex ) { shop in ThumbnailTileView( - image: shopImages[shop.id], + image: $shopImages[shop.id], title: shop.name, subTitle: nil, description: shop.access, diff --git a/HotSpot/Sources/Presentation/Search/Component/SearchResults.swift b/HotSpot/Sources/Presentation/Search/Component/SearchResults.swift index 196d873..0b45ebf 100644 --- a/HotSpot/Sources/Presentation/Search/Component/SearchResults.swift +++ b/HotSpot/Sources/Presentation/Search/Component/SearchResults.swift @@ -24,7 +24,7 @@ struct SearchResults: View { LazyVStack(spacing: 16) { ForEach(shops) { shop in ThumbnailTileView( - image: shopImages[shop.id], + image: $shopImages[shop.id], title: shop.name, subTitle: nil, description: shop.access, diff --git a/Tuist/Package.resolved b/Tuist/Package.resolved index 14271fe..6b25a83 100644 --- a/Tuist/Package.resolved +++ b/Tuist/Package.resolved @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/CobyLibrary/CobyDS.git", "state" : { - "revision" : "4dc0668cd4efc2719e4c1e51d288b55b3f0ca2c6", - "version" : "1.7.8" + "revision" : "d82fa21a7e96cf9f5c675d1e00358ed95523dae9", + "version" : "1.8.0" } }, { diff --git a/Tuist/Package.swift b/Tuist/Package.swift index 853f809..9f02596 100644 --- a/Tuist/Package.swift +++ b/Tuist/Package.swift @@ -26,7 +26,7 @@ let package = Package( .iOS(.v15) ], dependencies: [ - .package(url: "https://github.com/CobyLibrary/CobyDS.git", from: "1.7.8"), + .package(url: "https://github.com/CobyLibrary/CobyDS.git", from: "1.8.0"), .package(url: "https://github.com/Moya/Moya.git", from: "15.0.3"), .package(url: "https://github.com/pointfreeco/swift-composable-architecture.git", from: "1.19.1"), .package(url: "https://github.com/onevcat/Kingfisher.git", from: "8.3.2") From 99d10df1dfde32a283e0ffae8db0e7ccd450c30f Mon Sep 17 00:00:00 2001 From: coby5502 Date: Tue, 22 Apr 2025 00:01:13 +0900 Subject: [PATCH 15/32] [FEAT] set genre --- .../ShopRepository+Dependency.swift | 1 + .../Dependency/UserDefaults+Dependency.swift | 61 +++++++ .../Sources/Domain/Model/Filter/Genre.swift | 91 +++++++++++ HotSpot/Sources/Domain/Model/ShopGenre.swift | 72 --------- .../Map/Component/ShopAnnotationView.swift | 9 +- .../Presentation/Search/SearchStore.swift | 10 +- .../Presentation/Search/SearchView.swift | 2 +- .../Component/BudgetSection.swift | 44 +++++ .../Component/DistanceSection.swift | 44 +++++ .../Component/FeaturesSection.swift | 70 ++++++++ .../SearchFilter/Component/GenreSection.swift | 31 ++++ .../SearchFilter/SearchFilterStore.swift | 126 ++++++--------- .../SearchFilter/SearchFilterView.swift | 152 +++++------------- .../Component/ShopInfoSection.swift | 8 +- .../Component/ShopLocationMapView.swift | 22 +-- Tuist/Package.resolved | 4 +- Tuist/Package.swift | 2 +- 17 files changed, 468 insertions(+), 281 deletions(-) create mode 100644 HotSpot/Sources/Common/Dependency/UserDefaults+Dependency.swift create mode 100644 HotSpot/Sources/Domain/Model/Filter/Genre.swift delete mode 100644 HotSpot/Sources/Domain/Model/ShopGenre.swift create mode 100644 HotSpot/Sources/Presentation/SearchFilter/Component/BudgetSection.swift create mode 100644 HotSpot/Sources/Presentation/SearchFilter/Component/DistanceSection.swift create mode 100644 HotSpot/Sources/Presentation/SearchFilter/Component/FeaturesSection.swift create mode 100644 HotSpot/Sources/Presentation/SearchFilter/Component/GenreSection.swift diff --git a/HotSpot/Sources/Common/Dependency/ShopRepository+Dependency.swift b/HotSpot/Sources/Common/Dependency/ShopRepository+Dependency.swift index 5bed993..9af0c58 100644 --- a/HotSpot/Sources/Common/Dependency/ShopRepository+Dependency.swift +++ b/HotSpot/Sources/Common/Dependency/ShopRepository+Dependency.swift @@ -1,3 +1,4 @@ +import Foundation import ComposableArchitecture private enum ShopRepositoryKey: DependencyKey { diff --git a/HotSpot/Sources/Common/Dependency/UserDefaults+Dependency.swift b/HotSpot/Sources/Common/Dependency/UserDefaults+Dependency.swift new file mode 100644 index 0000000..a5c4297 --- /dev/null +++ b/HotSpot/Sources/Common/Dependency/UserDefaults+Dependency.swift @@ -0,0 +1,61 @@ +import Foundation +import ComposableArchitecture + +private enum UserDefaultsKey: DependencyKey { + static let liveValue = UserDefaults.standard +} + +extension DependencyValues { + var userDefaults: UserDefaults { + get { self[UserDefaultsKey.self] } + set { self[UserDefaultsKey.self] = newValue } + } +} + +// MARK: - Filter Keys +extension UserDefaults { + enum FilterKey { + static let distance = "distance" + static let budget = "budget" + static let genre = "genre" + static let wifi = "wifi" + static let privateRoom = "private_room" + static let nonSmoking = "non_smoking" + static let parking = "parking" + } + + var distance: Int { + get { integer(forKey: FilterKey.distance) } + set { set(newValue, forKey: FilterKey.distance) } + } + + var budget: Int { + get { integer(forKey: FilterKey.budget) } + set { set(newValue, forKey: FilterKey.budget) } + } + + var genre: String { + get { string(forKey: FilterKey.genre) ?? "" } + set { set(newValue, forKey: FilterKey.genre) } + } + + var wifi: Bool { + get { bool(forKey: FilterKey.wifi) } + set { set(newValue, forKey: FilterKey.wifi) } + } + + var privateRoom: Bool { + get { bool(forKey: FilterKey.privateRoom) } + set { set(newValue, forKey: FilterKey.privateRoom) } + } + + var nonSmoking: Bool { + get { bool(forKey: FilterKey.nonSmoking) } + set { set(newValue, forKey: FilterKey.nonSmoking) } + } + + var parking: Bool { + get { bool(forKey: FilterKey.parking) } + set { set(newValue, forKey: FilterKey.parking) } + } +} \ No newline at end of file diff --git a/HotSpot/Sources/Domain/Model/Filter/Genre.swift b/HotSpot/Sources/Domain/Model/Filter/Genre.swift new file mode 100644 index 0000000..7e37adb --- /dev/null +++ b/HotSpot/Sources/Domain/Model/Filter/Genre.swift @@ -0,0 +1,91 @@ +import UIKit + +public enum Genre: String, CaseIterable { + case izakaya = "G001" // 居酒屋 + case diningBar = "G002" // ダイニングバー・バル + case creative = "G003" // 創作料理 + case japanese = "G004" // 和食 + case western = "G005" // 洋食 + case italianFrench = "G006" // イタリアン・フレンチ + case chinese = "G007" // 中華 + case yakiniku = "G008" // 焼肉・ホルモン + case asian = "G009" // アジア・エスニック料理 + case international = "G010" // 各国料理 + case karaoke = "G011" // カラオケ・パーティ + case bar = "G012" // バー・カクテル + case ramen = "G013" // ラーメン + case cafe = "G014" // カフェ・スイーツ + case okonomiyaki = "G016" // お好み焼き・もんじゃ + case korean = "G017" // 韓国料理 + case other = "G015" // その他グルメ + + public var name: String { + switch self { + case .izakaya: return "居酒屋" + case .diningBar: return "ダイニングバー・バル" + case .creative: return "創作料理" + case .japanese: return "和食" + case .western: return "洋食" + case .italianFrench: return "イタリアン・フレンチ" + case .chinese: return "中華" + case .yakiniku: return "焼肉・ホルモン" + case .asian: return "アジア・エスニック料理" + case .international: return "各国料理" + case .karaoke: return "カラオケ・パーティ" + case .bar: return "バー・カクテル" + case .ramen: return "ラーメン" + case .cafe: return "カフェ・スイーツ" + case .okonomiyaki: return "お好み焼き・もんじゃ" + case .korean: return "韓国料理" + case .other: return "その他グルメ" + } + } + + public var color: UIColor { + switch self { + case .izakaya: return UIColor(red: 0.8, green: 0.2, blue: 0.2, alpha: 1.0) + case .diningBar: return UIColor(red: 0.6, green: 0.4, blue: 0.8, alpha: 1.0) + case .creative: return UIColor(red: 0.2, green: 0.6, blue: 0.8, alpha: 1.0) + case .japanese: return UIColor(red: 0.8, green: 0.6, blue: 0.2, alpha: 1.0) + case .western: return UIColor(red: 0.4, green: 0.8, blue: 0.4, alpha: 1.0) + case .italianFrench: return UIColor(red: 0.8, green: 0.4, blue: 0.6, alpha: 1.0) + case .chinese: return UIColor(red: 0.8, green: 0.2, blue: 0.4, alpha: 1.0) + case .yakiniku: return UIColor(red: 0.4, green: 0.2, blue: 0.2, alpha: 1.0) + case .asian: return UIColor(red: 0.2, green: 0.4, blue: 0.6, alpha: 1.0) + case .international: return UIColor(red: 0.4, green: 0.6, blue: 0.2, alpha: 1.0) + case .karaoke: return UIColor(red: 0.8, green: 0.4, blue: 0.2, alpha: 1.0) + case .bar: return UIColor(red: 0.6, green: 0.2, blue: 0.6, alpha: 1.0) + case .ramen: return UIColor(red: 0.2, green: 0.8, blue: 0.6, alpha: 1.0) + case .cafe: return UIColor(red: 0.6, green: 0.8, blue: 0.2, alpha: 1.0) + case .okonomiyaki: return UIColor(red: 0.8, green: 0.6, blue: 0.4, alpha: 1.0) + case .korean: return UIColor(red: 0.6, green: 0.2, blue: 0.2, alpha: 1.0) + case .other: return UIColor(red: 0.4, green: 0.4, blue: 0.4, alpha: 1.0) + } + } + + public var image: UIImage? { + switch self { + case .izakaya: return UIImage(systemName: "wineglass.fill") + case .diningBar: return UIImage(systemName: "wineglass") + case .creative: return UIImage(systemName: "fork.knife") + case .japanese: return UIImage(systemName: "leaf.fill") + case .western: return UIImage(systemName: "fork.knife.circle") + case .italianFrench: return UIImage(systemName: "fork.knife.circle.fill") + case .chinese: return UIImage(systemName: "bowl.fill") + case .yakiniku: return UIImage(systemName: "flame.fill") + case .asian: return UIImage(systemName: "globe.asia.australia.fill") + case .international: return UIImage(systemName: "globe") + case .karaoke: return UIImage(systemName: "music.mic") + case .bar: return UIImage(systemName: "wineglass") + case .ramen: return UIImage(systemName: "bowl") + case .cafe: return UIImage(systemName: "cup.and.saucer.fill") + case .okonomiyaki: return UIImage(systemName: "flame") + case .korean: return UIImage(systemName: "bowl") + case .other: return UIImage(systemName: "questionmark.circle.fill") + } + } + + public static func from(code: String) -> Genre? { + return Genre(rawValue: code) + } +} diff --git a/HotSpot/Sources/Domain/Model/ShopGenre.swift b/HotSpot/Sources/Domain/Model/ShopGenre.swift deleted file mode 100644 index dd3b190..0000000 --- a/HotSpot/Sources/Domain/Model/ShopGenre.swift +++ /dev/null @@ -1,72 +0,0 @@ -import UIKit - -public enum ShopGenre { - public static func name(for genreCode: String) -> String { - switch genreCode { - case "G001": return "居酒屋" - case "G002": return "ダイニングバー・バル" - case "G003": return "創作料理" - case "G004": return "和食" - case "G005": return "洋食" - case "G006": return "イタリアン・フレンチ" - case "G007": return "中華" - case "G008": return "焼肉・ホルモン" - case "G009": return "アジア・エスニック料理" - case "G010": return "各国料理" - case "G011": return "カラオケ・パーティ" - case "G012": return "バー・カクテル" - case "G013": return "ラーメン" - case "G014": return "カフェ・スイーツ" - case "G015": return "その他グルメ" - case "G016": return "お好み焼き・もんじゃ" - case "G017": return "韓国料理" - default: return "その他" - } - } - - public static func color(for genreCode: String) -> UIColor { - switch genreCode { - case "G001": return UIColor(red: 0.8, green: 0.2, blue: 0.2, alpha: 1.0) // 居酒屋 - Red - case "G002": return UIColor(red: 0.6, green: 0.4, blue: 0.8, alpha: 1.0) // ダイニングバー・バル - Purple - case "G003": return UIColor(red: 0.2, green: 0.6, blue: 0.8, alpha: 1.0) // 創作料理 - Blue - case "G004": return UIColor(red: 0.8, green: 0.6, blue: 0.2, alpha: 1.0) // 和食 - Orange - case "G005": return UIColor(red: 0.4, green: 0.8, blue: 0.4, alpha: 1.0) // 洋食 - Green - case "G006": return UIColor(red: 0.8, green: 0.4, blue: 0.6, alpha: 1.0) // イタリアン・フレンチ - Pink - case "G007": return UIColor(red: 0.8, green: 0.2, blue: 0.4, alpha: 1.0) // 中華 - Deep Pink - case "G008": return UIColor(red: 0.4, green: 0.2, blue: 0.2, alpha: 1.0) // 焼肉・ホルモン - Brown - case "G017": return UIColor(red: 0.6, green: 0.2, blue: 0.2, alpha: 1.0) // 韓国料理 - Dark Red - case "G009": return UIColor(red: 0.2, green: 0.4, blue: 0.6, alpha: 1.0) // アジア・エスニック料理 - Dark Blue - case "G010": return UIColor(red: 0.4, green: 0.6, blue: 0.2, alpha: 1.0) // 各国料理 - Olive - case "G011": return UIColor(red: 0.8, green: 0.4, blue: 0.2, alpha: 1.0) // カラオケ・パーティ - Orange - case "G012": return UIColor(red: 0.6, green: 0.2, blue: 0.6, alpha: 1.0) // バー・カクテル - Purple - case "G013": return UIColor(red: 0.2, green: 0.8, blue: 0.6, alpha: 1.0) // ラーメン - Teal - case "G016": return UIColor(red: 0.8, green: 0.6, blue: 0.4, alpha: 1.0) // お好み焼き・もんじゃ - Light Brown - case "G014": return UIColor(red: 0.6, green: 0.8, blue: 0.2, alpha: 1.0) // カフェ・スイーツ - Light Green - case "G015": return UIColor(red: 0.4, green: 0.4, blue: 0.4, alpha: 1.0) // その他グルメ - Gray - default: return .black - } - } - - public static func image(for genreCode: String) -> UIImage? { - switch genreCode { - case "G001": return UIImage(systemName: "wineglass.fill") // 居酒屋 - case "G002": return UIImage(systemName: "wineglass") // ダイニングバー・バル - case "G003": return UIImage(systemName: "fork.knife") // 創作料理 - case "G004": return UIImage(systemName: "leaf.fill") // 和食 - case "G005": return UIImage(systemName: "fork.knife.circle") // 洋食 - case "G006": return UIImage(systemName: "fork.knife.circle.fill") // イタリアン・フレンチ - case "G007": return UIImage(systemName: "bowl.fill") // 中華 - case "G008": return UIImage(systemName: "flame.fill") // 焼肉・ホルモン - case "G017": return UIImage(systemName: "bowl") // 韓国料理 - case "G009": return UIImage(systemName: "globe.asia.australia.fill") // アジア・エスニック料理 - case "G010": return UIImage(systemName: "globe") // 各国料理 - case "G011": return UIImage(systemName: "music.mic") // カラオケ・パーティ - case "G012": return UIImage(systemName: "wineglass") // バー・カクテル - case "G013": return UIImage(systemName: "bowl") // ラーメン - case "G016": return UIImage(systemName: "flame") // お好み焼き・もんじゃ - case "G014": return UIImage(systemName: "cup.and.saucer.fill") // カフェ・スイーツ - case "G015": return UIImage(systemName: "questionmark.circle.fill") // その他グルメ - default: return UIImage(systemName: "mappin.circle.fill") - } - } -} \ No newline at end of file diff --git a/HotSpot/Sources/Presentation/Map/Component/ShopAnnotationView.swift b/HotSpot/Sources/Presentation/Map/Component/ShopAnnotationView.swift index e11cbd2..4fa9586 100644 --- a/HotSpot/Sources/Presentation/Map/Component/ShopAnnotationView.swift +++ b/HotSpot/Sources/Presentation/Map/Component/ShopAnnotationView.swift @@ -10,9 +10,12 @@ class ShopAnnotationView: MKMarkerAnnotationView { clusteringIdentifier = "Shop" canShowCallout = false isEnabled = true - markerTintColor = ShopGenre.color(for: shopAnnotation.genreCode) - if let originalImage = ShopGenre.image(for: shopAnnotation.genreCode) { - glyphImage = originalImage.withTintColor(.white, renderingMode: .alwaysTemplate) + + if let genre = Genre.from(code: shopAnnotation.genreCode) { + markerTintColor = genre.color + if let originalImage = genre.image { + glyphImage = originalImage.withTintColor(.white, renderingMode: .alwaysTemplate) + } } } } diff --git a/HotSpot/Sources/Presentation/Search/SearchStore.swift b/HotSpot/Sources/Presentation/Search/SearchStore.swift index cc50953..e489264 100644 --- a/HotSpot/Sources/Presentation/Search/SearchStore.swift +++ b/HotSpot/Sources/Presentation/Search/SearchStore.swift @@ -13,7 +13,11 @@ struct SearchStore { var error: ShopError? = nil var currentLocation: MapCoordinate? var paginationState: PaginationState = .init() - var filterState: SearchFilterStore.State = .init() + var filterState: SearchFilterStore.State + + init() { + self.filterState = SearchFilterStore.State() + } } enum Action { @@ -76,7 +80,7 @@ struct SearchStore { range: state.filterState.selectedDistance, count: nil, keyword: text, - genre: state.filterState.selectedCuisine > 0 ? String(state.filterState.selectedCuisine) : nil, + genre: !state.filterState.selectedGenreCode.isEmpty ? state.filterState.selectedGenreCode : nil, order: nil, start: nil, budget: state.filterState.selectedBudget > 0 ? String(state.filterState.selectedBudget) : nil, @@ -114,7 +118,7 @@ struct SearchStore { range: state.filterState.selectedDistance, count: nil, keyword: state.searchText, - genre: state.filterState.selectedCuisine > 0 ? String(state.filterState.selectedCuisine) : nil, + genre: !state.filterState.selectedGenreCode.isEmpty ? state.filterState.selectedGenreCode : nil, order: nil, start: nil, budget: state.filterState.selectedBudget > 0 ? String(state.filterState.selectedBudget) : nil, diff --git a/HotSpot/Sources/Presentation/Search/SearchView.swift b/HotSpot/Sources/Presentation/Search/SearchView.swift index 31fc940..16bcd4d 100644 --- a/HotSpot/Sources/Presentation/Search/SearchView.swift +++ b/HotSpot/Sources/Presentation/Search/SearchView.swift @@ -18,7 +18,7 @@ struct SearchView: View { coordinator?.pop() }, rightSide: .icon, - rightIcon: UIImage.icMore, + rightIcon: UIImage.icFilter, rightAction: { coordinator?.showSearchFilter() } diff --git a/HotSpot/Sources/Presentation/SearchFilter/Component/BudgetSection.swift b/HotSpot/Sources/Presentation/SearchFilter/Component/BudgetSection.swift new file mode 100644 index 0000000..cdc512b --- /dev/null +++ b/HotSpot/Sources/Presentation/SearchFilter/Component/BudgetSection.swift @@ -0,0 +1,44 @@ +import SwiftUI + +struct BudgetSection: View { + let selectedBudget: Int + let onBudgetSelected: (Int) -> Void + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + Text("予算") + .font(.system(size: 17, weight: .semibold)) + .padding(.horizontal, 16) + + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 8) { + ForEach([0, 1, 2, 3, 4], id: \.self) { budget in + Button { + onBudgetSelected(budget) + } label: { + Text(budgetText(for: budget)) + .font(.system(size: 14, weight: .medium)) + .padding(.horizontal, 16) + .padding(.vertical, 8) + .background(selectedBudget == budget ? Color.blue : Color(.systemGray6)) + .foregroundColor(selectedBudget == budget ? .white : .primary) + .cornerRadius(16) + } + } + } + .padding(.horizontal, 16) + } + } + } + + private func budgetText(for budget: Int) -> String { + switch budget { + case 0: return "指定なし" + case 1: return "¥1,000~" + case 2: return "¥3,000~" + case 3: return "¥5,000~" + case 4: return "¥10,000~" + default: return "指定なし" + } + } +} \ No newline at end of file diff --git a/HotSpot/Sources/Presentation/SearchFilter/Component/DistanceSection.swift b/HotSpot/Sources/Presentation/SearchFilter/Component/DistanceSection.swift new file mode 100644 index 0000000..f5e0382 --- /dev/null +++ b/HotSpot/Sources/Presentation/SearchFilter/Component/DistanceSection.swift @@ -0,0 +1,44 @@ +import SwiftUI + +struct DistanceSection: View { + let selectedDistance: Int + let onDistanceSelected: (Int) -> Void + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + Text("距離") + .font(.system(size: 17, weight: .semibold)) + .padding(.horizontal, 16) + + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 8) { + ForEach([1, 2, 3, 4, 5], id: \.self) { distance in + Button { + onDistanceSelected(distance) + } label: { + Text(distanceText(for: distance)) + .font(.system(size: 14, weight: .medium)) + .padding(.horizontal, 16) + .padding(.vertical, 8) + .background(selectedDistance == distance ? Color.blue : Color(.systemGray6)) + .foregroundColor(selectedDistance == distance ? .white : .primary) + .cornerRadius(16) + } + } + } + .padding(.horizontal, 16) + } + } + } + + private func distanceText(for distance: Int) -> String { + switch distance { + case 1: return "300m" + case 2: return "500m" + case 3: return "1km" + case 4: return "2km" + case 5: return "3km" + default: return "1km" + } + } +} \ No newline at end of file diff --git a/HotSpot/Sources/Presentation/SearchFilter/Component/FeaturesSection.swift b/HotSpot/Sources/Presentation/SearchFilter/Component/FeaturesSection.swift new file mode 100644 index 0000000..7bab8d3 --- /dev/null +++ b/HotSpot/Sources/Presentation/SearchFilter/Component/FeaturesSection.swift @@ -0,0 +1,70 @@ +import SwiftUI + +struct FeaturesSection: View { + let hasWiFi: Bool + let hasPrivateRoom: Bool + let isNonSmoking: Bool + let hasParking: Bool + let onWiFiTapped: () -> Void + let onPrivateRoomTapped: () -> Void + let onNonSmokingTapped: () -> Void + let onParkingTapped: () -> Void + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + Text("設備・サービス") + .font(.system(size: 17, weight: .semibold)) + .padding(.horizontal, 16) + + VStack(spacing: 16) { + FeatureToggle( + title: "Wi-Fiあり", + isOn: hasWiFi, + onToggle: onWiFiTapped + ) + + FeatureToggle( + title: "個室あり", + isOn: hasPrivateRoom, + onToggle: onPrivateRoomTapped + ) + + FeatureToggle( + title: "禁煙", + isOn: isNonSmoking, + onToggle: onNonSmokingTapped + ) + + FeatureToggle( + title: "駐車場あり", + isOn: hasParking, + onToggle: onParkingTapped + ) + } + .padding(.horizontal, 16) + } + } +} + +private struct FeatureToggle: View { + let title: String + let isOn: Bool + let onToggle: () -> Void + + var body: some View { + HStack { + Text(title) + .font(.system(size: 16)) + Spacer() + Toggle("", isOn: .init( + get: { isOn }, + set: { _ in onToggle() } + )) + .labelsHidden() + } + .padding(.horizontal, 16) + .padding(.vertical, 12) + .background(Color(.systemGray6)) + .cornerRadius(12) + } +} \ No newline at end of file diff --git a/HotSpot/Sources/Presentation/SearchFilter/Component/GenreSection.swift b/HotSpot/Sources/Presentation/SearchFilter/Component/GenreSection.swift new file mode 100644 index 0000000..d9900f9 --- /dev/null +++ b/HotSpot/Sources/Presentation/SearchFilter/Component/GenreSection.swift @@ -0,0 +1,31 @@ +import SwiftUI + +struct GenreSection: View { + let selectedGenreCode: String + let onGenreSelected: (String) -> Void + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + Text("ジャンル") + .font(.system(size: 16, weight: .bold)) + + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 8) { + ForEach(Genre.allCases, id: \.rawValue) { genre in + Button { + onGenreSelected(genre.rawValue) + } label: { + Text(genre.name) + .font(.system(size: 14)) + .padding(.horizontal, 16) + .padding(.vertical, 8) + .background(selectedGenreCode == genre.rawValue ? Color.blue : Color.gray.opacity(0.1)) + .foregroundColor(selectedGenreCode == genre.rawValue ? .white : .black) + .cornerRadius(16) + } + } + } + } + } + } +} diff --git a/HotSpot/Sources/Presentation/SearchFilter/SearchFilterStore.swift b/HotSpot/Sources/Presentation/SearchFilter/SearchFilterStore.swift index 01820f8..326f3b5 100644 --- a/HotSpot/Sources/Presentation/SearchFilter/SearchFilterStore.swift +++ b/HotSpot/Sources/Presentation/SearchFilter/SearchFilterStore.swift @@ -3,107 +3,87 @@ import ComposableArchitecture @Reducer struct SearchFilterStore { - private enum UserDefaultsKey { - static let budget = "shop_filter_budget" - static let hasWiFi = "shop_filter_has_wifi" - static let hasPrivateRoom = "shop_filter_has_private_room" - static let isNonSmoking = "shop_filter_is_non_smoking" - static let hasParking = "shop_filter_has_parking" - static let cuisine = "shop_filter_cuisine" - static let distance = "shop_filter_distance" - } - + @Dependency(\.userDefaults) var userDefaults + struct State: Equatable { + var selectedDistance: Int var selectedBudget: Int - var hasWiFi: Bool var hasPrivateRoom: Bool + var hasWiFi: Bool var isNonSmoking: Bool - var hasParking: Bool - var selectedCuisine: Int - var selectedDistance: Int + var selectedGenreCode: String - init() { - let defaults = UserDefaults.standard - self.selectedBudget = defaults.integer(forKey: UserDefaultsKey.budget) - self.hasWiFi = defaults.bool(forKey: UserDefaultsKey.hasWiFi) - self.hasPrivateRoom = defaults.bool(forKey: UserDefaultsKey.hasPrivateRoom) - self.isNonSmoking = defaults.bool(forKey: UserDefaultsKey.isNonSmoking) - self.hasParking = defaults.bool(forKey: UserDefaultsKey.hasParking) - self.selectedCuisine = defaults.integer(forKey: UserDefaultsKey.cuisine) - self.selectedDistance = defaults.integer(forKey: UserDefaultsKey.distance) + init( + selectedDistance: Int = 0, + selectedBudget: Int = 0, + hasPrivateRoom: Bool = false, + hasWiFi: Bool = false, + isNonSmoking: Bool = false, + selectedGenreCode: String = "" + ) { + self.selectedDistance = selectedDistance + self.selectedBudget = selectedBudget + self.hasPrivateRoom = hasPrivateRoom + self.hasWiFi = hasWiFi + self.isNonSmoking = isNonSmoking + self.selectedGenreCode = selectedGenreCode } } - enum Action { - case updateBudget(Int) - case toggleWiFi - case togglePrivateRoom - case toggleNonSmoking - case toggleParking - case updateCuisine(Int) + enum Action: Equatable { case updateDistance(Int) + case updateBudget(Int) + case updatePrivateRoom(Bool) + case updateWiFi(Bool) + case updateNonSmoking(Bool) + case updateGenre(String) case resetFilters - case applyFilters } + init() {} + var body: some ReducerOf { Reduce { state, action in switch action { - case let .updateBudget(budget): - state.selectedBudget = budget - UserDefaults.standard.set(budget, forKey: UserDefaultsKey.budget) - return .none - - case .toggleWiFi: - state.hasWiFi.toggle() - UserDefaults.standard.set(state.hasWiFi, forKey: UserDefaultsKey.hasWiFi) + case let .updateDistance(distance): + state.selectedDistance = distance + userDefaults.distance = distance return .none - case .togglePrivateRoom: - state.hasPrivateRoom.toggle() - UserDefaults.standard.set(state.hasPrivateRoom, forKey: UserDefaultsKey.hasPrivateRoom) + case let .updateBudget(budget): + state.selectedBudget = budget + userDefaults.budget = budget return .none - case .toggleNonSmoking: - state.isNonSmoking.toggle() - UserDefaults.standard.set(state.isNonSmoking, forKey: UserDefaultsKey.isNonSmoking) + case let .updatePrivateRoom(hasPrivateRoom): + state.hasPrivateRoom = hasPrivateRoom + userDefaults.privateRoom = hasPrivateRoom return .none - case .toggleParking: - state.hasParking.toggle() - UserDefaults.standard.set(state.hasParking, forKey: UserDefaultsKey.hasParking) + case let .updateWiFi(hasWiFi): + state.hasWiFi = hasWiFi + userDefaults.wifi = hasWiFi return .none - case let .updateCuisine(cuisine): - state.selectedCuisine = cuisine - UserDefaults.standard.set(cuisine, forKey: UserDefaultsKey.cuisine) + case let .updateNonSmoking(isNonSmoking): + state.isNonSmoking = isNonSmoking + userDefaults.nonSmoking = isNonSmoking return .none - case let .updateDistance(distance): - state.selectedDistance = distance - UserDefaults.standard.set(distance, forKey: UserDefaultsKey.distance) + case let .updateGenre(genreCode): + state.selectedGenreCode = genreCode + userDefaults.genre = genreCode 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 - - UserDefaults.standard.removeObject(forKey: UserDefaultsKey.budget) - UserDefaults.standard.removeObject(forKey: UserDefaultsKey.hasWiFi) - UserDefaults.standard.removeObject(forKey: UserDefaultsKey.hasPrivateRoom) - UserDefaults.standard.removeObject(forKey: UserDefaultsKey.isNonSmoking) - UserDefaults.standard.removeObject(forKey: UserDefaultsKey.hasParking) - UserDefaults.standard.removeObject(forKey: UserDefaultsKey.cuisine) - UserDefaults.standard.removeObject(forKey: UserDefaultsKey.distance) - - return .none - - case .applyFilters: + state = State( + selectedDistance: userDefaults.distance, + selectedBudget: userDefaults.budget, + hasPrivateRoom: userDefaults.privateRoom, + hasWiFi: userDefaults.wifi, + isNonSmoking: userDefaults.nonSmoking, + selectedGenreCode: userDefaults.genre + ) return .none } } diff --git a/HotSpot/Sources/Presentation/SearchFilter/SearchFilterView.swift b/HotSpot/Sources/Presentation/SearchFilter/SearchFilterView.swift index 5c356c3..87ba899 100644 --- a/HotSpot/Sources/Presentation/SearchFilter/SearchFilterView.swift +++ b/HotSpot/Sources/Presentation/SearchFilter/SearchFilterView.swift @@ -1,125 +1,51 @@ import SwiftUI import ComposableArchitecture +import CobyDS struct SearchFilterView: View { let store: StoreOf - @Environment(\.dismiss) private var dismiss + @Environment(\.coordinator) private var coordinator var body: some View { WithViewStore(store, observe: { $0 }) { viewStore in - NavigationView { - FilterForm(viewStore: viewStore) - } - } - } -} - -private struct FilterForm: View { - let viewStore: ViewStore - @Environment(\.dismiss) private var dismiss - - 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) + VStack(spacing: 16) { + DistanceSection( + selectedDistance: viewStore.selectedDistance, + onDistanceSelected: { viewStore.send(.updateDistance($0)) } + ) + + BudgetSection( + selectedBudget: viewStore.selectedBudget, + onBudgetSelected: { viewStore.send(.updateBudget($0)) } + ) + + GenreSection( + selectedGenreCode: viewStore.selectedGenreCode, + onGenreSelected: { viewStore.send(.updateGenre($0)) } + ) + + VStack(spacing: 8) { + Toggle("WiFi", isOn: viewStore.binding( + get: \.hasWiFi, + send: { .updateWiFi($0) } + )) + + Toggle("개인실", isOn: viewStore.binding( + get: \.hasPrivateRoom, + send: { .updatePrivateRoom($0) } + )) + + Toggle("금연", isOn: viewStore.binding( + get: \.isNonSmoking, + send: { .updateNonSmoking($0) } + )) } - } - ToolbarItem(placement: .navigationBarTrailing) { - Button("Apply") { - viewStore.send(.applyFilters) - dismiss() + .padding(.horizontal) + + Button("필터 초기화") { + viewStore.send(.resetFilters) } - } - } - } -} - -private struct BudgetSection: View { - let viewStore: ViewStore - - var body: some View { - Section(header: Text("Budget")) { - Picker("Budget", selection: viewStore.binding( - get: \.selectedBudget, - send: SearchFilterStore.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: SearchFilterStore.Action.toggleWiFi - )) - Toggle("Private Room", isOn: viewStore.binding( - get: \.hasPrivateRoom, - send: SearchFilterStore.Action.togglePrivateRoom - )) - Toggle("Non-Smoking", isOn: viewStore.binding( - get: \.isNonSmoking, - send: SearchFilterStore.Action.toggleNonSmoking - )) - Toggle("Parking Available", isOn: viewStore.binding( - get: \.hasParking, - send: SearchFilterStore.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: SearchFilterStore.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: SearchFilterStore.Action.updateDistance - )) { - Text("300m").tag(1) - Text("500m").tag(2) - Text("1km").tag(3) - Text("2km").tag(4) - Text("3km").tag(5) + .padding() } } } @@ -132,4 +58,4 @@ private struct DistanceSection: View { reducer: { SearchFilterStore() } ) ) -} +} diff --git a/HotSpot/Sources/Presentation/ShopDetail/Component/ShopInfoSection.swift b/HotSpot/Sources/Presentation/ShopDetail/Component/ShopInfoSection.swift index f6db786..4b40735 100644 --- a/HotSpot/Sources/Presentation/ShopDetail/Component/ShopInfoSection.swift +++ b/HotSpot/Sources/Presentation/ShopDetail/Component/ShopInfoSection.swift @@ -13,9 +13,11 @@ struct ShopInfoSection: View { .font(.pretendard(size: 24, weight: .bold)) .foregroundColor(Color.labelNormal) - Text(ShopGenre.name(for: shop.genreCode)) - .font(.pretendard(size: 14, weight: .regular)) - .foregroundColor(Color.labelAlternative) + if let genre = Genre.from(code: shop.genreCode) { + Text(genre.name) + .font(.pretendard(size: 14, weight: .regular)) + .foregroundColor(Color.labelAlternative) + } } // Address diff --git a/HotSpot/Sources/Presentation/ShopDetail/Component/ShopLocationMapView.swift b/HotSpot/Sources/Presentation/ShopDetail/Component/ShopLocationMapView.swift index 46e7638..818efd4 100644 --- a/HotSpot/Sources/Presentation/ShopDetail/Component/ShopLocationMapView.swift +++ b/HotSpot/Sources/Presentation/ShopDetail/Component/ShopLocationMapView.swift @@ -17,17 +17,19 @@ struct ShopLocationMapView: View { Map(coordinateRegion: .constant(region), annotationItems: [ShopPin(shop: shop)]) { pin in MapAnnotation(coordinate: pin.coordinate) { - ZStack { - Circle() - .fill(Color(uiColor: ShopGenre.color(for: pin.genreCode))) - .frame(width: 40, height: 40) + if let genre = Genre.from(code: pin.genreCode) { + ZStack { + Circle() + .fill(Color(uiColor: genre.color)) + .frame(width: 40, height: 40) - Image(uiImage: ShopGenre.image(for: pin.genreCode) ?? UIImage()) - .resizable() - .renderingMode(.template) - .foregroundColor(.white) - .scaledToFit() - .frame(width: 24, height: 24) + Image(uiImage: genre.image ?? UIImage()) + .resizable() + .renderingMode(.template) + .foregroundColor(.white) + .scaledToFit() + .frame(width: 24, height: 24) + } } } } diff --git a/Tuist/Package.resolved b/Tuist/Package.resolved index 6b25a83..082f03e 100644 --- a/Tuist/Package.resolved +++ b/Tuist/Package.resolved @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/CobyLibrary/CobyDS.git", "state" : { - "revision" : "d82fa21a7e96cf9f5c675d1e00358ed95523dae9", - "version" : "1.8.0" + "revision" : "d889250752085dc9eb8d3770c3a6935cb0518971", + "version" : "1.8.1" } }, { diff --git a/Tuist/Package.swift b/Tuist/Package.swift index 9f02596..7021af9 100644 --- a/Tuist/Package.swift +++ b/Tuist/Package.swift @@ -26,7 +26,7 @@ let package = Package( .iOS(.v15) ], dependencies: [ - .package(url: "https://github.com/CobyLibrary/CobyDS.git", from: "1.8.0"), + .package(url: "https://github.com/CobyLibrary/CobyDS.git", from: "1.8.1"), .package(url: "https://github.com/Moya/Moya.git", from: "15.0.3"), .package(url: "https://github.com/pointfreeco/swift-composable-architecture.git", from: "1.19.1"), .package(url: "https://github.com/onevcat/Kingfisher.git", from: "8.3.2") From adb45eff154cc97f2fb23b5da68fb706d8862d66 Mon Sep 17 00:00:00 2001 From: coby5502 Date: Tue, 22 Apr 2025 00:13:40 +0900 Subject: [PATCH 16/32] [CHORE] filter --- .../Dependency/UserDefaults+Dependency.swift | 48 ++++++------ .../Sources/Domain/Model/Filter/Budget.swift | 75 +++++++++++++++++++ .../Presentation/Search/SearchStore.swift | 37 ++++----- .../Component/BudgetSection.swift | 25 ++----- ...stanceSection.swift => RangeSection.swift} | 22 +++--- .../SearchFilter/SearchFilterStore.swift | 71 ++++++++---------- .../SearchFilter/SearchFilterView.swift | 14 ++-- 7 files changed, 171 insertions(+), 121 deletions(-) create mode 100644 HotSpot/Sources/Domain/Model/Filter/Budget.swift rename HotSpot/Sources/Presentation/SearchFilter/Component/{DistanceSection.swift => RangeSection.swift} (60%) diff --git a/HotSpot/Sources/Common/Dependency/UserDefaults+Dependency.swift b/HotSpot/Sources/Common/Dependency/UserDefaults+Dependency.swift index a5c4297..0c4fcc3 100644 --- a/HotSpot/Sources/Common/Dependency/UserDefaults+Dependency.swift +++ b/HotSpot/Sources/Common/Dependency/UserDefaults+Dependency.swift @@ -14,48 +14,48 @@ extension DependencyValues { // MARK: - Filter Keys extension UserDefaults { - enum FilterKey { - static let distance = "distance" - static let budget = "budget" - static let genre = "genre" - static let wifi = "wifi" - static let privateRoom = "private_room" - static let nonSmoking = "non_smoking" - static let parking = "parking" + private enum FilterKey: String { + case range = "range" + case budget = "budget" + case genre = "genre" + case wifi = "wifi" + case privateRoom = "privateRoom" + case nonSmoking = "nonSmoking" + case parking = "parking" } - var distance: Int { - get { integer(forKey: FilterKey.distance) } - set { set(newValue, forKey: FilterKey.distance) } + var range: Int { + get { integer(forKey: FilterKey.range.rawValue) } + set { set(newValue, forKey: FilterKey.range.rawValue) } } - var budget: Int { - get { integer(forKey: FilterKey.budget) } - set { set(newValue, forKey: FilterKey.budget) } + var budget: String { + get { string(forKey: FilterKey.budget.rawValue) ?? "" } + set { set(newValue, forKey: FilterKey.budget.rawValue) } } var genre: String { - get { string(forKey: FilterKey.genre) ?? "" } - set { set(newValue, forKey: FilterKey.genre) } + get { string(forKey: FilterKey.genre.rawValue) ?? "" } + set { set(newValue, forKey: FilterKey.genre.rawValue) } } var wifi: Bool { - get { bool(forKey: FilterKey.wifi) } - set { set(newValue, forKey: FilterKey.wifi) } + get { bool(forKey: FilterKey.wifi.rawValue) } + set { set(newValue, forKey: FilterKey.wifi.rawValue) } } var privateRoom: Bool { - get { bool(forKey: FilterKey.privateRoom) } - set { set(newValue, forKey: FilterKey.privateRoom) } + get { bool(forKey: FilterKey.privateRoom.rawValue) } + set { set(newValue, forKey: FilterKey.privateRoom.rawValue) } } var nonSmoking: Bool { - get { bool(forKey: FilterKey.nonSmoking) } - set { set(newValue, forKey: FilterKey.nonSmoking) } + get { bool(forKey: FilterKey.nonSmoking.rawValue) } + set { set(newValue, forKey: FilterKey.nonSmoking.rawValue) } } var parking: Bool { - get { bool(forKey: FilterKey.parking) } - set { set(newValue, forKey: FilterKey.parking) } + get { bool(forKey: FilterKey.parking.rawValue) } + set { set(newValue, forKey: FilterKey.parking.rawValue) } } } \ No newline at end of file diff --git a/HotSpot/Sources/Domain/Model/Filter/Budget.swift b/HotSpot/Sources/Domain/Model/Filter/Budget.swift new file mode 100644 index 0000000..8f859e3 --- /dev/null +++ b/HotSpot/Sources/Domain/Model/Filter/Budget.swift @@ -0,0 +1,75 @@ +import UIKit + +public enum Budget: String, CaseIterable { + case under500 = "B009" // ~500円 + case from501to1000 = "B010" // 501~1000円 + case from1001to1500 = "B011" // 1001~1500円 + case from1501to2000 = "B001" // 1501~2000円 + case from2001to3000 = "B002" // 2001~3000円 + case from3001to4000 = "B003" // 3001~4000円 + case from4001to5000 = "B008" // 4001~5000円 + case from5001to7000 = "B004" // 5001~7000円 + case from7001to10000 = "B005" // 7001~10000円 + case from10001to15000 = "B006" // 10001~15000円 + case from15001to20000 = "B012" // 15001~20000円 + case from20001to30000 = "B013" // 20001~30000円 + case over30001 = "B014" // 30001円~ + + public var name: String { + switch self { + case .under500: return "~500円" + case .from501to1000: return "501~1000円" + case .from1001to1500: return "1001~1500円" + case .from1501to2000: return "1501~2000円" + case .from2001to3000: return "2001~3000円" + case .from3001to4000: return "3001~4000円" + case .from4001to5000: return "4001~5000円" + case .from5001to7000: return "5001~7000円" + case .from7001to10000: return "7001~10000円" + case .from10001to15000: return "10001~15000円" + case .from15001to20000: return "15001~20000円" + case .from20001to30000: return "20001~30000円" + case .over30001: return "30001円~" + } + } + + public var minPrice: Int { + switch self { + case .under500: return 0 + case .from501to1000: return 501 + case .from1001to1500: return 1001 + case .from1501to2000: return 1501 + case .from2001to3000: return 2001 + case .from3001to4000: return 3001 + case .from4001to5000: return 4001 + case .from5001to7000: return 5001 + case .from7001to10000: return 7001 + case .from10001to15000: return 10001 + case .from15001to20000: return 15001 + case .from20001to30000: return 20001 + case .over30001: return 30001 + } + } + + public var maxPrice: Int? { + switch self { + case .under500: return 500 + case .from501to1000: return 1000 + case .from1001to1500: return 1500 + case .from1501to2000: return 2000 + case .from2001to3000: return 3000 + case .from3001to4000: return 4000 + case .from4001to5000: return 5000 + case .from5001to7000: return 7000 + case .from7001to10000: return 10000 + case .from10001to15000: return 15000 + case .from15001to20000: return 20000 + case .from20001to30000: return 30000 + case .over30001: return nil + } + } + + public static func from(code: String) -> Budget? { + return Budget(rawValue: code) + } +} \ No newline at end of file diff --git a/HotSpot/Sources/Presentation/Search/SearchStore.swift b/HotSpot/Sources/Presentation/Search/SearchStore.swift index e489264..44b9ea0 100644 --- a/HotSpot/Sources/Presentation/Search/SearchStore.swift +++ b/HotSpot/Sources/Presentation/Search/SearchStore.swift @@ -6,6 +6,7 @@ import ComposableArchitecture struct SearchStore { @Dependency(\.shopRepository) var shopRepository @Dependency(\.locationManager) var locationManager + @Dependency(\.userDefaults) var userDefaults struct State: Equatable { var shops: [ShopModel] = [] @@ -13,11 +14,6 @@ struct SearchStore { var error: ShopError? = nil var currentLocation: MapCoordinate? var paginationState: PaginationState = .init() - var filterState: SearchFilterStore.State - - init() { - self.filterState = SearchFilterStore.State() - } } enum Action { @@ -29,7 +25,6 @@ struct SearchStore { case clearError case loadMore case updatePaginationState(PaginationState) - case updateFilterState(SearchFilterStore.State) } var body: some ReducerOf { @@ -77,16 +72,16 @@ struct SearchStore { let request = ShopSearchRequestDTO( lat: location.latitude, lng: location.longitude, - range: state.filterState.selectedDistance, + range: userDefaults.range, count: nil, keyword: text, - genre: !state.filterState.selectedGenreCode.isEmpty ? state.filterState.selectedGenreCode : nil, + genre: !userDefaults.genre.isEmpty ? userDefaults.genre : nil, order: nil, start: nil, - budget: state.filterState.selectedBudget > 0 ? String(state.filterState.selectedBudget) : nil, - privateRoom: state.filterState.hasPrivateRoom ? true : nil, - wifi: state.filterState.hasWiFi ? true : nil, - nonSmoking: state.filterState.isNonSmoking ? true : nil, + budget: !userDefaults.budget.isEmpty ? userDefaults.budget : nil, + privateRoom: userDefaults.privateRoom ? true : nil, + wifi: userDefaults.wifi ? true : nil, + nonSmoking: userDefaults.nonSmoking ? true : nil, coupon: nil, openNow: nil ) @@ -115,16 +110,16 @@ struct SearchStore { let request = ShopSearchRequestDTO( lat: location.latitude, lng: location.longitude, - range: state.filterState.selectedDistance, + range: userDefaults.range, count: nil, keyword: state.searchText, - genre: !state.filterState.selectedGenreCode.isEmpty ? state.filterState.selectedGenreCode : nil, + genre: !userDefaults.genre.isEmpty ? userDefaults.genre : nil, order: nil, start: nil, - budget: state.filterState.selectedBudget > 0 ? String(state.filterState.selectedBudget) : nil, - privateRoom: state.filterState.hasPrivateRoom ? true : nil, - wifi: state.filterState.hasWiFi ? true : nil, - nonSmoking: state.filterState.isNonSmoking ? true : nil, + budget: !userDefaults.budget.isEmpty ? userDefaults.budget : nil, + privateRoom: userDefaults.privateRoom ? true : nil, + wifi: userDefaults.wifi ? true : nil, + nonSmoking: userDefaults.nonSmoking ? true : nil, coupon: nil, openNow: nil ) @@ -153,12 +148,6 @@ struct SearchStore { case let .updatePaginationState(paginationState): state.paginationState = paginationState return .none - - case let .updateFilterState(filterState): - state.filterState = filterState - return .run { [state] send in - await send(.search(state.searchText)) - } } } } diff --git a/HotSpot/Sources/Presentation/SearchFilter/Component/BudgetSection.swift b/HotSpot/Sources/Presentation/SearchFilter/Component/BudgetSection.swift index cdc512b..7350219 100644 --- a/HotSpot/Sources/Presentation/SearchFilter/Component/BudgetSection.swift +++ b/HotSpot/Sources/Presentation/SearchFilter/Component/BudgetSection.swift @@ -1,8 +1,8 @@ import SwiftUI struct BudgetSection: View { - let selectedBudget: Int - let onBudgetSelected: (Int) -> Void + let selectedBudgetCode: String + let onBudgetSelected: (String) -> Void var body: some View { VStack(alignment: .leading, spacing: 12) { @@ -12,16 +12,16 @@ struct BudgetSection: View { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 8) { - ForEach([0, 1, 2, 3, 4], id: \.self) { budget in + ForEach(Budget.allCases, id: \.self) { budget in Button { - onBudgetSelected(budget) + onBudgetSelected(budget.rawValue) } label: { - Text(budgetText(for: budget)) + Text(budget.name) .font(.system(size: 14, weight: .medium)) .padding(.horizontal, 16) .padding(.vertical, 8) - .background(selectedBudget == budget ? Color.blue : Color(.systemGray6)) - .foregroundColor(selectedBudget == budget ? .white : .primary) + .background(selectedBudgetCode == budget.rawValue ? Color.blue : Color(.systemGray6)) + .foregroundColor(selectedBudgetCode == budget.rawValue ? .white : .primary) .cornerRadius(16) } } @@ -30,15 +30,4 @@ struct BudgetSection: View { } } } - - private func budgetText(for budget: Int) -> String { - switch budget { - case 0: return "指定なし" - case 1: return "¥1,000~" - case 2: return "¥3,000~" - case 3: return "¥5,000~" - case 4: return "¥10,000~" - default: return "指定なし" - } - } } \ No newline at end of file diff --git a/HotSpot/Sources/Presentation/SearchFilter/Component/DistanceSection.swift b/HotSpot/Sources/Presentation/SearchFilter/Component/RangeSection.swift similarity index 60% rename from HotSpot/Sources/Presentation/SearchFilter/Component/DistanceSection.swift rename to HotSpot/Sources/Presentation/SearchFilter/Component/RangeSection.swift index f5e0382..3b08c9c 100644 --- a/HotSpot/Sources/Presentation/SearchFilter/Component/DistanceSection.swift +++ b/HotSpot/Sources/Presentation/SearchFilter/Component/RangeSection.swift @@ -1,27 +1,27 @@ import SwiftUI -struct DistanceSection: View { - let selectedDistance: Int - let onDistanceSelected: (Int) -> Void +struct RangeSection: View { + let selectedRange: Int + let onRangeSelected: (Int) -> Void var body: some View { VStack(alignment: .leading, spacing: 12) { - Text("距離") + Text("거리") .font(.system(size: 17, weight: .semibold)) .padding(.horizontal, 16) ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 8) { - ForEach([1, 2, 3, 4, 5], id: \.self) { distance in + ForEach([1, 2, 3, 4, 5], id: \.self) { range in Button { - onDistanceSelected(distance) + onRangeSelected(range) } label: { - Text(distanceText(for: distance)) + Text(rangeText(for: range)) .font(.system(size: 14, weight: .medium)) .padding(.horizontal, 16) .padding(.vertical, 8) - .background(selectedDistance == distance ? Color.blue : Color(.systemGray6)) - .foregroundColor(selectedDistance == distance ? .white : .primary) + .background(selectedRange == range ? Color.blue : Color(.systemGray6)) + .foregroundColor(selectedRange == range ? .white : .primary) .cornerRadius(16) } } @@ -31,8 +31,8 @@ struct DistanceSection: View { } } - private func distanceText(for distance: Int) -> String { - switch distance { + private func rangeText(for range: Int) -> String { + switch range { case 1: return "300m" case 2: return "500m" case 3: return "1km" diff --git a/HotSpot/Sources/Presentation/SearchFilter/SearchFilterStore.swift b/HotSpot/Sources/Presentation/SearchFilter/SearchFilterStore.swift index 326f3b5..3fd356b 100644 --- a/HotSpot/Sources/Presentation/SearchFilter/SearchFilterStore.swift +++ b/HotSpot/Sources/Presentation/SearchFilter/SearchFilterStore.swift @@ -6,37 +6,37 @@ struct SearchFilterStore { @Dependency(\.userDefaults) var userDefaults struct State: Equatable { - var selectedDistance: Int - var selectedBudget: Int - var hasPrivateRoom: Bool + var selectedRange: Int + var selectedBudget: String + var selectedGenreCode: String var hasWiFi: Bool + var hasPrivateRoom: Bool var isNonSmoking: Bool - var selectedGenreCode: String init( - selectedDistance: Int = 0, - selectedBudget: Int = 0, - hasPrivateRoom: Bool = false, - hasWiFi: Bool = false, - isNonSmoking: Bool = false, - selectedGenreCode: String = "" + selectedRange: Int = UserDefaults.standard.range, + selectedBudget: String = UserDefaults.standard.budget, + selectedGenreCode: String = UserDefaults.standard.genre, + hasWiFi: Bool = UserDefaults.standard.wifi, + hasPrivateRoom: Bool = UserDefaults.standard.privateRoom, + isNonSmoking: Bool = UserDefaults.standard.nonSmoking ) { - self.selectedDistance = selectedDistance + self.selectedRange = selectedRange self.selectedBudget = selectedBudget - self.hasPrivateRoom = hasPrivateRoom + self.selectedGenreCode = selectedGenreCode self.hasWiFi = hasWiFi + self.hasPrivateRoom = hasPrivateRoom self.isNonSmoking = isNonSmoking - self.selectedGenreCode = selectedGenreCode } } enum Action: Equatable { - case updateDistance(Int) - case updateBudget(Int) - case updatePrivateRoom(Bool) + case updateRange(Int) + case updateBudget(String) + case updateGenre(String) case updateWiFi(Bool) + case updatePrivateRoom(Bool) case updateNonSmoking(Bool) - case updateGenre(String) case resetFilters } @@ -45,45 +45,38 @@ struct SearchFilterStore { var body: some ReducerOf { Reduce { state, action in switch action { - case let .updateDistance(distance): - state.selectedDistance = distance - userDefaults.distance = distance + case let .updateRange(range): + state.selectedRange = range + UserDefaults.standard.range = range return .none case let .updateBudget(budget): state.selectedBudget = budget - userDefaults.budget = budget + UserDefaults.standard.budget = budget return .none - case let .updatePrivateRoom(hasPrivateRoom): - state.hasPrivateRoom = hasPrivateRoom - userDefaults.privateRoom = hasPrivateRoom + case let .updateGenre(genre): + state.selectedGenreCode = genre + UserDefaults.standard.genre = genre return .none case let .updateWiFi(hasWiFi): state.hasWiFi = hasWiFi - userDefaults.wifi = hasWiFi + UserDefaults.standard.wifi = hasWiFi return .none - case let .updateNonSmoking(isNonSmoking): - state.isNonSmoking = isNonSmoking - userDefaults.nonSmoking = isNonSmoking + case let .updatePrivateRoom(hasPrivateRoom): + state.hasPrivateRoom = hasPrivateRoom + UserDefaults.standard.privateRoom = hasPrivateRoom return .none - case let .updateGenre(genreCode): - state.selectedGenreCode = genreCode - userDefaults.genre = genreCode + case let .updateNonSmoking(isNonSmoking): + state.isNonSmoking = isNonSmoking + UserDefaults.standard.nonSmoking = isNonSmoking return .none case .resetFilters: - state = State( - selectedDistance: userDefaults.distance, - selectedBudget: userDefaults.budget, - hasPrivateRoom: userDefaults.privateRoom, - hasWiFi: userDefaults.wifi, - isNonSmoking: userDefaults.nonSmoking, - selectedGenreCode: userDefaults.genre - ) + state = State() return .none } } diff --git a/HotSpot/Sources/Presentation/SearchFilter/SearchFilterView.swift b/HotSpot/Sources/Presentation/SearchFilter/SearchFilterView.swift index 87ba899..03a1595 100644 --- a/HotSpot/Sources/Presentation/SearchFilter/SearchFilterView.swift +++ b/HotSpot/Sources/Presentation/SearchFilter/SearchFilterView.swift @@ -9,14 +9,18 @@ struct SearchFilterView: View { var body: some View { WithViewStore(store, observe: { $0 }) { viewStore in VStack(spacing: 16) { - DistanceSection( - selectedDistance: viewStore.selectedDistance, - onDistanceSelected: { viewStore.send(.updateDistance($0)) } + RangeSection( + selectedRange: viewStore.selectedRange, + onRangeSelected: { range in + viewStore.send(.updateRange(range)) + } ) BudgetSection( - selectedBudget: viewStore.selectedBudget, - onBudgetSelected: { viewStore.send(.updateBudget($0)) } + selectedBudgetCode: viewStore.selectedBudget, + onBudgetSelected: { budgetCode in + viewStore.send(.updateBudget(budgetCode)) + } ) GenreSection( From 56c9624d5c3e45c83111821662828739063463c6 Mon Sep 17 00:00:00 2001 From: coby5502 Date: Tue, 22 Apr 2025 00:24:02 +0900 Subject: [PATCH 17/32] [FEAT] genre --- .../Dependency/UserDefaults+Dependency.swift | 8 ++++---- .../Data/DTO/Request/ShopSearchRequestDTO.swift | 4 ++-- .../UseCase/InfiniteScrollSearchUseCase.swift | 2 +- HotSpot/Sources/Domain/UseCase/ShopsUseCase.swift | 2 +- .../Sources/Presentation/Search/SearchStore.swift | 4 ++-- .../SearchFilter/Component/GenreSection.swift | 14 ++++++++------ .../SearchFilter/SearchFilterStore.swift | 14 +++++++------- .../SearchFilter/SearchFilterView.swift | 12 ++++++++++-- 8 files changed, 35 insertions(+), 25 deletions(-) diff --git a/HotSpot/Sources/Common/Dependency/UserDefaults+Dependency.swift b/HotSpot/Sources/Common/Dependency/UserDefaults+Dependency.swift index 0c4fcc3..d33d4f0 100644 --- a/HotSpot/Sources/Common/Dependency/UserDefaults+Dependency.swift +++ b/HotSpot/Sources/Common/Dependency/UserDefaults+Dependency.swift @@ -17,7 +17,7 @@ extension UserDefaults { private enum FilterKey: String { case range = "range" case budget = "budget" - case genre = "genre" + case genres = "genres" case wifi = "wifi" case privateRoom = "privateRoom" case nonSmoking = "nonSmoking" @@ -34,9 +34,9 @@ extension UserDefaults { set { set(newValue, forKey: FilterKey.budget.rawValue) } } - var genre: String { - get { string(forKey: FilterKey.genre.rawValue) ?? "" } - set { set(newValue, forKey: FilterKey.genre.rawValue) } + var genres: [String] { + get { stringArray(forKey: FilterKey.genres.rawValue) ?? [] } + set { set(newValue, forKey: FilterKey.genres.rawValue) } } var wifi: Bool { diff --git a/HotSpot/Sources/Data/DTO/Request/ShopSearchRequestDTO.swift b/HotSpot/Sources/Data/DTO/Request/ShopSearchRequestDTO.swift index 55cd645..9d2c4ac 100644 --- a/HotSpot/Sources/Data/DTO/Request/ShopSearchRequestDTO.swift +++ b/HotSpot/Sources/Data/DTO/Request/ShopSearchRequestDTO.swift @@ -6,7 +6,7 @@ struct ShopSearchRequestDTO { let range: Int // Search range (1–5) let count: Int? // Number of results (1–100) let keyword: String? // Keyword search - let genre: String? // Genre code + let genres: [String]? // Genre codes let order: Int? // Order: 1=recommend, 2=popularity let start: Int? // Starting index for paging let budget: String? // Budget code @@ -26,7 +26,7 @@ struct ShopSearchRequestDTO { if let count = count { params["count"] = count } if let keyword = keyword { params["keyword"] = keyword } - if let genre = genre { params["genre"] = genre } + if let genres = genres, !genres.isEmpty { params["genre"] = genres.joined(separator: ",") } if let order = order { params["order"] = order } if let start = start { params["start"] = start } if let budget = budget { params["budget"] = budget } diff --git a/HotSpot/Sources/Domain/UseCase/InfiniteScrollSearchUseCase.swift b/HotSpot/Sources/Domain/UseCase/InfiniteScrollSearchUseCase.swift index 7fb3bc1..c6a9628 100644 --- a/HotSpot/Sources/Domain/UseCase/InfiniteScrollSearchUseCase.swift +++ b/HotSpot/Sources/Domain/UseCase/InfiniteScrollSearchUseCase.swift @@ -23,7 +23,7 @@ struct InfiniteScrollSearchUseCase { range: request.range, count: pageSize, keyword: request.keyword, - genre: request.genre, + genres: request.genres, order: request.order, start: start, budget: request.budget, diff --git a/HotSpot/Sources/Domain/UseCase/ShopsUseCase.swift b/HotSpot/Sources/Domain/UseCase/ShopsUseCase.swift index dfddd83..acd5965 100644 --- a/HotSpot/Sources/Domain/UseCase/ShopsUseCase.swift +++ b/HotSpot/Sources/Domain/UseCase/ShopsUseCase.swift @@ -14,7 +14,7 @@ struct ShopsUseCase { range: 5, count: nil, keyword: nil, - genre: nil, + genres: nil, order: nil, start: nil, budget: nil, diff --git a/HotSpot/Sources/Presentation/Search/SearchStore.swift b/HotSpot/Sources/Presentation/Search/SearchStore.swift index 44b9ea0..6d3485e 100644 --- a/HotSpot/Sources/Presentation/Search/SearchStore.swift +++ b/HotSpot/Sources/Presentation/Search/SearchStore.swift @@ -75,7 +75,7 @@ struct SearchStore { range: userDefaults.range, count: nil, keyword: text, - genre: !userDefaults.genre.isEmpty ? userDefaults.genre : nil, + genres: !userDefaults.genres.isEmpty ? userDefaults.genres : nil, order: nil, start: nil, budget: !userDefaults.budget.isEmpty ? userDefaults.budget : nil, @@ -113,7 +113,7 @@ struct SearchStore { range: userDefaults.range, count: nil, keyword: state.searchText, - genre: !userDefaults.genre.isEmpty ? userDefaults.genre : nil, + genres: !userDefaults.genres.isEmpty ? userDefaults.genres : nil, order: nil, start: nil, budget: !userDefaults.budget.isEmpty ? userDefaults.budget : nil, diff --git a/HotSpot/Sources/Presentation/SearchFilter/Component/GenreSection.swift b/HotSpot/Sources/Presentation/SearchFilter/Component/GenreSection.swift index d9900f9..cb491fa 100644 --- a/HotSpot/Sources/Presentation/SearchFilter/Component/GenreSection.swift +++ b/HotSpot/Sources/Presentation/SearchFilter/Component/GenreSection.swift @@ -1,30 +1,32 @@ import SwiftUI struct GenreSection: View { - let selectedGenreCode: String + let selectedGenres: [String] let onGenreSelected: (String) -> Void var body: some View { VStack(alignment: .leading, spacing: 12) { Text("ジャンル") - .font(.system(size: 16, weight: .bold)) + .font(.system(size: 17, weight: .semibold)) + .padding(.horizontal, 16) ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 8) { - ForEach(Genre.allCases, id: \.rawValue) { genre in + ForEach(Genre.allCases, id: \.self) { genre in Button { onGenreSelected(genre.rawValue) } label: { Text(genre.name) - .font(.system(size: 14)) + .font(.system(size: 14, weight: .medium)) .padding(.horizontal, 16) .padding(.vertical, 8) - .background(selectedGenreCode == genre.rawValue ? Color.blue : Color.gray.opacity(0.1)) - .foregroundColor(selectedGenreCode == genre.rawValue ? .white : .black) + .background(selectedGenres.contains(genre.rawValue) ? Color.blue : Color(.systemGray6)) + .foregroundColor(selectedGenres.contains(genre.rawValue) ? .white : .primary) .cornerRadius(16) } } } + .padding(.horizontal, 16) } } } diff --git a/HotSpot/Sources/Presentation/SearchFilter/SearchFilterStore.swift b/HotSpot/Sources/Presentation/SearchFilter/SearchFilterStore.swift index 3fd356b..b5e39b1 100644 --- a/HotSpot/Sources/Presentation/SearchFilter/SearchFilterStore.swift +++ b/HotSpot/Sources/Presentation/SearchFilter/SearchFilterStore.swift @@ -8,7 +8,7 @@ struct SearchFilterStore { struct State: Equatable { var selectedRange: Int var selectedBudget: String - var selectedGenreCode: String + var selectedGenres: [String] var hasWiFi: Bool var hasPrivateRoom: Bool var isNonSmoking: Bool @@ -16,14 +16,14 @@ struct SearchFilterStore { init( selectedRange: Int = UserDefaults.standard.range, selectedBudget: String = UserDefaults.standard.budget, - selectedGenreCode: String = UserDefaults.standard.genre, + selectedGenres: [String] = UserDefaults.standard.genres, hasWiFi: Bool = UserDefaults.standard.wifi, hasPrivateRoom: Bool = UserDefaults.standard.privateRoom, isNonSmoking: Bool = UserDefaults.standard.nonSmoking ) { self.selectedRange = selectedRange self.selectedBudget = selectedBudget - self.selectedGenreCode = selectedGenreCode + self.selectedGenres = selectedGenres self.hasWiFi = hasWiFi self.hasPrivateRoom = hasPrivateRoom self.isNonSmoking = isNonSmoking @@ -33,7 +33,7 @@ struct SearchFilterStore { enum Action: Equatable { case updateRange(Int) case updateBudget(String) - case updateGenre(String) + case updateGenres([String]) case updateWiFi(Bool) case updatePrivateRoom(Bool) case updateNonSmoking(Bool) @@ -55,9 +55,9 @@ struct SearchFilterStore { UserDefaults.standard.budget = budget return .none - case let .updateGenre(genre): - state.selectedGenreCode = genre - UserDefaults.standard.genre = genre + case let .updateGenres(genres): + state.selectedGenres = genres + UserDefaults.standard.genres = genres return .none case let .updateWiFi(hasWiFi): diff --git a/HotSpot/Sources/Presentation/SearchFilter/SearchFilterView.swift b/HotSpot/Sources/Presentation/SearchFilter/SearchFilterView.swift index 03a1595..c05ab2c 100644 --- a/HotSpot/Sources/Presentation/SearchFilter/SearchFilterView.swift +++ b/HotSpot/Sources/Presentation/SearchFilter/SearchFilterView.swift @@ -24,8 +24,16 @@ struct SearchFilterView: View { ) GenreSection( - selectedGenreCode: viewStore.selectedGenreCode, - onGenreSelected: { viewStore.send(.updateGenre($0)) } + selectedGenres: viewStore.selectedGenres, + onGenreSelected: { genreCode in + var updatedGenres = viewStore.selectedGenres + if updatedGenres.contains(genreCode) { + updatedGenres.removeAll { $0 == genreCode } + } else { + updatedGenres.append(genreCode) + } + viewStore.send(.updateGenres(updatedGenres)) + } ) VStack(spacing: 8) { From 1e43cb2b7da2fdf66a31f45bd4394f812ee1ee5d Mon Sep 17 00:00:00 2001 From: coby5502 Date: Tue, 22 Apr 2025 00:27:33 +0900 Subject: [PATCH 18/32] [FEAT] budgets --- .../Dependency/UserDefaults+Dependency.swift | 8 ++++---- .../Data/DTO/Request/ShopSearchRequestDTO.swift | 4 ++-- .../UseCase/InfiniteScrollSearchUseCase.swift | 2 +- HotSpot/Sources/Domain/UseCase/ShopsUseCase.swift | 2 +- .../Sources/Presentation/Search/SearchStore.swift | 4 ++-- .../SearchFilter/Component/BudgetSection.swift | 6 +++--- .../SearchFilter/SearchFilterStore.swift | 14 +++++++------- .../SearchFilter/SearchFilterView.swift | 10 ++++++++-- 8 files changed, 28 insertions(+), 22 deletions(-) diff --git a/HotSpot/Sources/Common/Dependency/UserDefaults+Dependency.swift b/HotSpot/Sources/Common/Dependency/UserDefaults+Dependency.swift index d33d4f0..5c07f25 100644 --- a/HotSpot/Sources/Common/Dependency/UserDefaults+Dependency.swift +++ b/HotSpot/Sources/Common/Dependency/UserDefaults+Dependency.swift @@ -16,7 +16,7 @@ extension DependencyValues { extension UserDefaults { private enum FilterKey: String { case range = "range" - case budget = "budget" + case budgets = "budgets" case genres = "genres" case wifi = "wifi" case privateRoom = "privateRoom" @@ -29,9 +29,9 @@ extension UserDefaults { set { set(newValue, forKey: FilterKey.range.rawValue) } } - var budget: String { - get { string(forKey: FilterKey.budget.rawValue) ?? "" } - set { set(newValue, forKey: FilterKey.budget.rawValue) } + var budgets: [String] { + get { stringArray(forKey: FilterKey.budgets.rawValue) ?? [] } + set { set(newValue, forKey: FilterKey.budgets.rawValue) } } var genres: [String] { diff --git a/HotSpot/Sources/Data/DTO/Request/ShopSearchRequestDTO.swift b/HotSpot/Sources/Data/DTO/Request/ShopSearchRequestDTO.swift index 9d2c4ac..ad85dc5 100644 --- a/HotSpot/Sources/Data/DTO/Request/ShopSearchRequestDTO.swift +++ b/HotSpot/Sources/Data/DTO/Request/ShopSearchRequestDTO.swift @@ -9,7 +9,7 @@ struct ShopSearchRequestDTO { let genres: [String]? // Genre codes let order: Int? // Order: 1=recommend, 2=popularity let start: Int? // Starting index for paging - let budget: String? // Budget code + let budgets: [String]? // Budget codes let privateRoom: Bool? // Private room availability let wifi: Bool? // Wi-Fi availability let nonSmoking: Bool? // Non-smoking availability @@ -29,7 +29,7 @@ struct ShopSearchRequestDTO { if let genres = genres, !genres.isEmpty { params["genre"] = genres.joined(separator: ",") } if let order = order { params["order"] = order } if let start = start { params["start"] = start } - if let budget = budget { params["budget"] = budget } + if let budgets = budgets, !budgets.isEmpty { params["budget"] = budgets.joined(separator: ",") } if let privateRoom = privateRoom { params["private_room"] = privateRoom ? 1 : 0 } if let wifi = wifi { params["wifi"] = wifi ? 1 : 0 } if let nonSmoking = nonSmoking { params["non_smoking"] = nonSmoking ? 1 : 0 } diff --git a/HotSpot/Sources/Domain/UseCase/InfiniteScrollSearchUseCase.swift b/HotSpot/Sources/Domain/UseCase/InfiniteScrollSearchUseCase.swift index c6a9628..642c343 100644 --- a/HotSpot/Sources/Domain/UseCase/InfiniteScrollSearchUseCase.swift +++ b/HotSpot/Sources/Domain/UseCase/InfiniteScrollSearchUseCase.swift @@ -26,7 +26,7 @@ struct InfiniteScrollSearchUseCase { genres: request.genres, order: request.order, start: start, - budget: request.budget, + budgets: request.budgets, privateRoom: request.privateRoom, wifi: request.wifi, nonSmoking: request.nonSmoking, diff --git a/HotSpot/Sources/Domain/UseCase/ShopsUseCase.swift b/HotSpot/Sources/Domain/UseCase/ShopsUseCase.swift index acd5965..08310be 100644 --- a/HotSpot/Sources/Domain/UseCase/ShopsUseCase.swift +++ b/HotSpot/Sources/Domain/UseCase/ShopsUseCase.swift @@ -17,7 +17,7 @@ struct ShopsUseCase { genres: nil, order: nil, start: nil, - budget: nil, + budgets: nil, privateRoom: nil, wifi: nil, nonSmoking: nil, diff --git a/HotSpot/Sources/Presentation/Search/SearchStore.swift b/HotSpot/Sources/Presentation/Search/SearchStore.swift index 6d3485e..0fbd336 100644 --- a/HotSpot/Sources/Presentation/Search/SearchStore.swift +++ b/HotSpot/Sources/Presentation/Search/SearchStore.swift @@ -78,7 +78,7 @@ struct SearchStore { genres: !userDefaults.genres.isEmpty ? userDefaults.genres : nil, order: nil, start: nil, - budget: !userDefaults.budget.isEmpty ? userDefaults.budget : nil, + budgets: !userDefaults.budgets.isEmpty ? userDefaults.budgets : nil, privateRoom: userDefaults.privateRoom ? true : nil, wifi: userDefaults.wifi ? true : nil, nonSmoking: userDefaults.nonSmoking ? true : nil, @@ -116,7 +116,7 @@ struct SearchStore { genres: !userDefaults.genres.isEmpty ? userDefaults.genres : nil, order: nil, start: nil, - budget: !userDefaults.budget.isEmpty ? userDefaults.budget : nil, + budgets: !userDefaults.budgets.isEmpty ? userDefaults.budgets : nil, privateRoom: userDefaults.privateRoom ? true : nil, wifi: userDefaults.wifi ? true : nil, nonSmoking: userDefaults.nonSmoking ? true : nil, diff --git a/HotSpot/Sources/Presentation/SearchFilter/Component/BudgetSection.swift b/HotSpot/Sources/Presentation/SearchFilter/Component/BudgetSection.swift index 7350219..4be8dad 100644 --- a/HotSpot/Sources/Presentation/SearchFilter/Component/BudgetSection.swift +++ b/HotSpot/Sources/Presentation/SearchFilter/Component/BudgetSection.swift @@ -1,7 +1,7 @@ import SwiftUI struct BudgetSection: View { - let selectedBudgetCode: String + let selectedBudgets: [String] let onBudgetSelected: (String) -> Void var body: some View { @@ -20,8 +20,8 @@ struct BudgetSection: View { .font(.system(size: 14, weight: .medium)) .padding(.horizontal, 16) .padding(.vertical, 8) - .background(selectedBudgetCode == budget.rawValue ? Color.blue : Color(.systemGray6)) - .foregroundColor(selectedBudgetCode == budget.rawValue ? .white : .primary) + .background(selectedBudgets.contains(budget.rawValue) ? Color.blue : Color(.systemGray6)) + .foregroundColor(selectedBudgets.contains(budget.rawValue) ? .white : .primary) .cornerRadius(16) } } diff --git a/HotSpot/Sources/Presentation/SearchFilter/SearchFilterStore.swift b/HotSpot/Sources/Presentation/SearchFilter/SearchFilterStore.swift index b5e39b1..d9f30a7 100644 --- a/HotSpot/Sources/Presentation/SearchFilter/SearchFilterStore.swift +++ b/HotSpot/Sources/Presentation/SearchFilter/SearchFilterStore.swift @@ -7,7 +7,7 @@ struct SearchFilterStore { struct State: Equatable { var selectedRange: Int - var selectedBudget: String + var selectedBudgets: [String] var selectedGenres: [String] var hasWiFi: Bool var hasPrivateRoom: Bool @@ -15,14 +15,14 @@ struct SearchFilterStore { init( selectedRange: Int = UserDefaults.standard.range, - selectedBudget: String = UserDefaults.standard.budget, + selectedBudgets: [String] = UserDefaults.standard.budgets, selectedGenres: [String] = UserDefaults.standard.genres, hasWiFi: Bool = UserDefaults.standard.wifi, hasPrivateRoom: Bool = UserDefaults.standard.privateRoom, isNonSmoking: Bool = UserDefaults.standard.nonSmoking ) { self.selectedRange = selectedRange - self.selectedBudget = selectedBudget + self.selectedBudgets = selectedBudgets self.selectedGenres = selectedGenres self.hasWiFi = hasWiFi self.hasPrivateRoom = hasPrivateRoom @@ -32,7 +32,7 @@ struct SearchFilterStore { enum Action: Equatable { case updateRange(Int) - case updateBudget(String) + case updateBudgets([String]) case updateGenres([String]) case updateWiFi(Bool) case updatePrivateRoom(Bool) @@ -50,9 +50,9 @@ struct SearchFilterStore { UserDefaults.standard.range = range return .none - case let .updateBudget(budget): - state.selectedBudget = budget - UserDefaults.standard.budget = budget + case let .updateBudgets(budgets): + state.selectedBudgets = budgets + UserDefaults.standard.budgets = budgets return .none case let .updateGenres(genres): diff --git a/HotSpot/Sources/Presentation/SearchFilter/SearchFilterView.swift b/HotSpot/Sources/Presentation/SearchFilter/SearchFilterView.swift index c05ab2c..86dd6ca 100644 --- a/HotSpot/Sources/Presentation/SearchFilter/SearchFilterView.swift +++ b/HotSpot/Sources/Presentation/SearchFilter/SearchFilterView.swift @@ -17,9 +17,15 @@ struct SearchFilterView: View { ) BudgetSection( - selectedBudgetCode: viewStore.selectedBudget, + selectedBudgets: viewStore.selectedBudgets, onBudgetSelected: { budgetCode in - viewStore.send(.updateBudget(budgetCode)) + var updatedBudgets = viewStore.selectedBudgets + if updatedBudgets.contains(budgetCode) { + updatedBudgets.removeAll { $0 == budgetCode } + } else if updatedBudgets.count < 2 { + updatedBudgets.append(budgetCode) + } + viewStore.send(.updateBudgets(updatedBudgets)) } ) From d4e76bdbff266ef6fb877af9b4abe915ac68de19 Mon Sep 17 00:00:00 2001 From: coby5502 Date: Tue, 22 Apr 2025 01:02:20 +0900 Subject: [PATCH 19/32] [FEAT] set filters --- .../Dependency/UserDefaults+Dependency.swift | 18 +-- .../DTO/Request/ShopSearchRequestDTO.swift | 22 ++-- .../UseCase/InfiniteScrollSearchUseCase.swift | 4 +- .../Sources/Domain/UseCase/ShopsUseCase.swift | 10 +- .../Presentation/Search/SearchStore.swift | 20 ++- .../Component/DistanceSection.swift | 33 +++++ .../Component/FeaturesSection.swift | 82 ++++++------ .../SearchFilter/Component/RangeSection.swift | 2 +- .../SearchFilter/SearchFilterStore.swift | 69 ++++++---- .../SearchFilter/SearchFilterView.swift | 119 ++++++++++-------- 10 files changed, 224 insertions(+), 155 deletions(-) create mode 100644 HotSpot/Sources/Presentation/SearchFilter/Component/DistanceSection.swift diff --git a/HotSpot/Sources/Common/Dependency/UserDefaults+Dependency.swift b/HotSpot/Sources/Common/Dependency/UserDefaults+Dependency.swift index 5c07f25..92a82ad 100644 --- a/HotSpot/Sources/Common/Dependency/UserDefaults+Dependency.swift +++ b/HotSpot/Sources/Common/Dependency/UserDefaults+Dependency.swift @@ -25,7 +25,7 @@ extension UserDefaults { } var range: Int { - get { integer(forKey: FilterKey.range.rawValue) } + get { integer(forKey: FilterKey.range.rawValue) == 0 ? 3 : integer(forKey: FilterKey.range.rawValue) } set { set(newValue, forKey: FilterKey.range.rawValue) } } @@ -39,23 +39,23 @@ extension UserDefaults { set { set(newValue, forKey: FilterKey.genres.rawValue) } } - var wifi: Bool { - get { bool(forKey: FilterKey.wifi.rawValue) } + var wifi: Int { + get { integer(forKey: FilterKey.wifi.rawValue) } set { set(newValue, forKey: FilterKey.wifi.rawValue) } } - var privateRoom: Bool { - get { bool(forKey: FilterKey.privateRoom.rawValue) } + var privateRoom: Int { + get { integer(forKey: FilterKey.privateRoom.rawValue) } set { set(newValue, forKey: FilterKey.privateRoom.rawValue) } } - var nonSmoking: Bool { - get { bool(forKey: FilterKey.nonSmoking.rawValue) } + var nonSmoking: Int { + get { integer(forKey: FilterKey.nonSmoking.rawValue) } set { set(newValue, forKey: FilterKey.nonSmoking.rawValue) } } - var parking: Bool { - get { bool(forKey: FilterKey.parking.rawValue) } + var parking: Int { + get { integer(forKey: FilterKey.parking.rawValue) } set { set(newValue, forKey: FilterKey.parking.rawValue) } } } \ No newline at end of file diff --git a/HotSpot/Sources/Data/DTO/Request/ShopSearchRequestDTO.swift b/HotSpot/Sources/Data/DTO/Request/ShopSearchRequestDTO.swift index ad85dc5..a04c472 100644 --- a/HotSpot/Sources/Data/DTO/Request/ShopSearchRequestDTO.swift +++ b/HotSpot/Sources/Data/DTO/Request/ShopSearchRequestDTO.swift @@ -7,34 +7,30 @@ struct ShopSearchRequestDTO { let count: Int? // Number of results (1–100) let keyword: String? // Keyword search let genres: [String]? // Genre codes - let order: Int? // Order: 1=recommend, 2=popularity let start: Int? // Starting index for paging let budgets: [String]? // Budget codes - let privateRoom: Bool? // Private room availability - let wifi: Bool? // Wi-Fi availability - let nonSmoking: Bool? // Non-smoking availability - let coupon: Bool? // Coupon availability - let openNow: Bool? // Currently open filter + let privateRoom: Int // Private room availability (0 or 1) + let wifi: Int // Wi-Fi availability (0 or 1) + let nonSmoking: Int // Non-smoking availability (0 or 1) + let parking: Int // Parking availability (0 or 1) /// Converts the DTO into a dictionary of parameters for Moya or URL encoding var asParameters: [String: Any] { var params: [String: Any] = [ "lat": lat, "lng": lng, - "range": range + "range": range, + "private_room": privateRoom, + "wifi": wifi, + "non_smoking": nonSmoking, + "parking": parking ] if let count = count { params["count"] = count } if let keyword = keyword { params["keyword"] = keyword } if let genres = genres, !genres.isEmpty { params["genre"] = genres.joined(separator: ",") } - if let order = order { params["order"] = order } if let start = start { params["start"] = start } if let budgets = budgets, !budgets.isEmpty { params["budget"] = budgets.joined(separator: ",") } - if let privateRoom = privateRoom { params["private_room"] = privateRoom ? 1 : 0 } - if let wifi = wifi { params["wifi"] = wifi ? 1 : 0 } - if let nonSmoking = nonSmoking { params["non_smoking"] = nonSmoking ? 1 : 0 } - if let coupon = coupon { params["coupon"] = coupon ? 1 : 0 } - if let openNow = openNow, openNow { params["open"] = "now" } return params } diff --git a/HotSpot/Sources/Domain/UseCase/InfiniteScrollSearchUseCase.swift b/HotSpot/Sources/Domain/UseCase/InfiniteScrollSearchUseCase.swift index 642c343..769f45d 100644 --- a/HotSpot/Sources/Domain/UseCase/InfiniteScrollSearchUseCase.swift +++ b/HotSpot/Sources/Domain/UseCase/InfiniteScrollSearchUseCase.swift @@ -24,14 +24,12 @@ struct InfiniteScrollSearchUseCase { count: pageSize, keyword: request.keyword, genres: request.genres, - order: request.order, start: start, budgets: request.budgets, privateRoom: request.privateRoom, wifi: request.wifi, nonSmoking: request.nonSmoking, - coupon: request.coupon, - openNow: request.openNow + parking: request.parking, ) let response = try await repository.searchShops(request: paginatedRequest) diff --git a/HotSpot/Sources/Domain/UseCase/ShopsUseCase.swift b/HotSpot/Sources/Domain/UseCase/ShopsUseCase.swift index 08310be..1e17345 100644 --- a/HotSpot/Sources/Domain/UseCase/ShopsUseCase.swift +++ b/HotSpot/Sources/Domain/UseCase/ShopsUseCase.swift @@ -15,14 +15,12 @@ struct ShopsUseCase { count: nil, keyword: nil, genres: nil, - order: nil, start: nil, budgets: nil, - privateRoom: nil, - wifi: nil, - nonSmoking: nil, - coupon: nil, - openNow: nil + privateRoom: 0, + wifi: 0, + nonSmoking: 0, + parking: 0 ) let response = try await repository.searchShops(request: request) diff --git a/HotSpot/Sources/Presentation/Search/SearchStore.swift b/HotSpot/Sources/Presentation/Search/SearchStore.swift index 0fbd336..7377039 100644 --- a/HotSpot/Sources/Presentation/Search/SearchStore.swift +++ b/HotSpot/Sources/Presentation/Search/SearchStore.swift @@ -76,14 +76,12 @@ struct SearchStore { count: nil, keyword: text, genres: !userDefaults.genres.isEmpty ? userDefaults.genres : nil, - order: nil, start: nil, budgets: !userDefaults.budgets.isEmpty ? userDefaults.budgets : nil, - privateRoom: userDefaults.privateRoom ? true : nil, - wifi: userDefaults.wifi ? true : nil, - nonSmoking: userDefaults.nonSmoking ? true : nil, - coupon: nil, - openNow: nil + privateRoom: userDefaults.privateRoom, + wifi: userDefaults.wifi, + nonSmoking: userDefaults.nonSmoking, + parking: userDefaults.parking ) let useCase = InfiniteScrollSearchUseCase(repository: shopRepository) @@ -114,14 +112,12 @@ struct SearchStore { count: nil, keyword: state.searchText, genres: !userDefaults.genres.isEmpty ? userDefaults.genres : nil, - order: nil, start: nil, budgets: !userDefaults.budgets.isEmpty ? userDefaults.budgets : nil, - privateRoom: userDefaults.privateRoom ? true : nil, - wifi: userDefaults.wifi ? true : nil, - nonSmoking: userDefaults.nonSmoking ? true : nil, - coupon: nil, - openNow: nil + privateRoom: userDefaults.privateRoom, + wifi: userDefaults.wifi, + nonSmoking: userDefaults.nonSmoking, + parking: userDefaults.parking ) let useCase = InfiniteScrollSearchUseCase(repository: shopRepository) diff --git a/HotSpot/Sources/Presentation/SearchFilter/Component/DistanceSection.swift b/HotSpot/Sources/Presentation/SearchFilter/Component/DistanceSection.swift new file mode 100644 index 0000000..139f5b0 --- /dev/null +++ b/HotSpot/Sources/Presentation/SearchFilter/Component/DistanceSection.swift @@ -0,0 +1,33 @@ +import SwiftUI + +struct DistanceSection: View { + let selectedDistance: String + let onDistanceSelected: (String) -> Void + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + Text("거리") + .font(.system(size: 17, weight: .semibold)) + .padding(.horizontal, 16) + + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 8) { + ForEach(["1", "2", "3", "5", "10"], id: \.self) { distance in + Button { + onDistanceSelected(distance) + } label: { + Text("\(distance)km") + .font(.system(size: 14, weight: .medium)) + .padding(.horizontal, 16) + .padding(.vertical, 8) + .background(selectedDistance == distance ? Color.blue : Color(.systemGray6)) + .foregroundColor(selectedDistance == distance ? .white : .primary) + .cornerRadius(16) + } + } + } + .padding(.horizontal, 16) + } + } + } +} \ No newline at end of file diff --git a/HotSpot/Sources/Presentation/SearchFilter/Component/FeaturesSection.swift b/HotSpot/Sources/Presentation/SearchFilter/Component/FeaturesSection.swift index 7bab8d3..0c6ca5d 100644 --- a/HotSpot/Sources/Presentation/SearchFilter/Component/FeaturesSection.swift +++ b/HotSpot/Sources/Presentation/SearchFilter/Component/FeaturesSection.swift @@ -1,70 +1,78 @@ import SwiftUI +import CobyDS struct FeaturesSection: View { - let hasWiFi: Bool - let hasPrivateRoom: Bool - let isNonSmoking: Bool - let hasParking: Bool + let wifi: Int + let privateRoom: Int + let nonSmoking: Int + let parking: Int let onWiFiTapped: () -> Void let onPrivateRoomTapped: () -> Void let onNonSmokingTapped: () -> Void let onParkingTapped: () -> Void var body: some View { - VStack(alignment: .leading, spacing: 12) { + VStack(alignment: .leading, spacing: 16) { Text("設備・サービス") - .font(.system(size: 17, weight: .semibold)) - .padding(.horizontal, 16) + .font(.system(size: 16, weight: .semibold)) - VStack(spacing: 16) { - FeatureToggle( + HStack(spacing: 8) { + FeatureButton( title: "Wi-Fiあり", - isOn: hasWiFi, - onToggle: onWiFiTapped + isSelected: wifi != 0, + action: onWiFiTapped ) - FeatureToggle( + FeatureButton( title: "個室あり", - isOn: hasPrivateRoom, - onToggle: onPrivateRoomTapped + isSelected: privateRoom != 0, + action: onPrivateRoomTapped ) - FeatureToggle( - title: "禁煙", - isOn: isNonSmoking, - onToggle: onNonSmokingTapped + FeatureButton( + title: "禁煙席あり", + isSelected: nonSmoking != 0, + action: onNonSmokingTapped ) - FeatureToggle( + FeatureButton( title: "駐車場あり", - isOn: hasParking, - onToggle: onParkingTapped + isSelected: parking != 0, + action: onParkingTapped ) } - .padding(.horizontal, 16) } + .padding(.horizontal, 16) } } -private struct FeatureToggle: View { +private struct FeatureButton: View { let title: String - let isOn: Bool - let onToggle: () -> Void + let isSelected: Bool + let action: () -> Void var body: some View { - HStack { + Button(action: action) { Text(title) - .font(.system(size: 16)) - Spacer() - Toggle("", isOn: .init( - get: { isOn }, - set: { _ in onToggle() } - )) - .labelsHidden() + .font(.system(size: 14)) + .foregroundColor(isSelected ? .white : .black) + .padding(.horizontal, 12) + .padding(.vertical, 8) + .background(isSelected ? Color.blue : Color(.systemGray6)) + .cornerRadius(16) } - .padding(.horizontal, 16) - .padding(.vertical, 12) - .background(Color(.systemGray6)) - .cornerRadius(12) } +} + +#Preview { + FeaturesSection( + wifi: 1, + privateRoom: 0, + nonSmoking: 1, + parking: 0, + onWiFiTapped: {}, + onPrivateRoomTapped: {}, + onNonSmokingTapped: {}, + onParkingTapped: {} + ) } \ No newline at end of file diff --git a/HotSpot/Sources/Presentation/SearchFilter/Component/RangeSection.swift b/HotSpot/Sources/Presentation/SearchFilter/Component/RangeSection.swift index 3b08c9c..ccc3c9c 100644 --- a/HotSpot/Sources/Presentation/SearchFilter/Component/RangeSection.swift +++ b/HotSpot/Sources/Presentation/SearchFilter/Component/RangeSection.swift @@ -6,7 +6,7 @@ struct RangeSection: View { var body: some View { VStack(alignment: .leading, spacing: 12) { - Text("거리") + Text("距離") .font(.system(size: 17, weight: .semibold)) .padding(.horizontal, 16) diff --git a/HotSpot/Sources/Presentation/SearchFilter/SearchFilterStore.swift b/HotSpot/Sources/Presentation/SearchFilter/SearchFilterStore.swift index d9f30a7..46eada7 100644 --- a/HotSpot/Sources/Presentation/SearchFilter/SearchFilterStore.swift +++ b/HotSpot/Sources/Presentation/SearchFilter/SearchFilterStore.swift @@ -9,24 +9,27 @@ struct SearchFilterStore { var selectedRange: Int var selectedBudgets: [String] var selectedGenres: [String] - var hasWiFi: Bool - var hasPrivateRoom: Bool - var isNonSmoking: Bool + var wifi: Int + var privateRoom: Int + var nonSmoking: Int + var parking: Int init( selectedRange: Int = UserDefaults.standard.range, selectedBudgets: [String] = UserDefaults.standard.budgets, selectedGenres: [String] = UserDefaults.standard.genres, - hasWiFi: Bool = UserDefaults.standard.wifi, - hasPrivateRoom: Bool = UserDefaults.standard.privateRoom, - isNonSmoking: Bool = UserDefaults.standard.nonSmoking + wifi: Int = UserDefaults.standard.wifi, + privateRoom: Int = UserDefaults.standard.privateRoom, + nonSmoking: Int = UserDefaults.standard.nonSmoking, + parking: Int = UserDefaults.standard.parking ) { self.selectedRange = selectedRange self.selectedBudgets = selectedBudgets self.selectedGenres = selectedGenres - self.hasWiFi = hasWiFi - self.hasPrivateRoom = hasPrivateRoom - self.isNonSmoking = isNonSmoking + self.wifi = wifi + self.privateRoom = privateRoom + self.nonSmoking = nonSmoking + self.parking = parking } } @@ -34,9 +37,11 @@ struct SearchFilterStore { case updateRange(Int) case updateBudgets([String]) case updateGenres([String]) - case updateWiFi(Bool) - case updatePrivateRoom(Bool) - case updateNonSmoking(Bool) + case updateWiFi(Int) + case updatePrivateRoom(Int) + case updateNonSmoking(Int) + case updateParking(Int) + case applyFilters case resetFilters } @@ -47,36 +52,50 @@ struct SearchFilterStore { switch action { case let .updateRange(range): state.selectedRange = range - UserDefaults.standard.range = range return .none case let .updateBudgets(budgets): state.selectedBudgets = budgets - UserDefaults.standard.budgets = budgets return .none case let .updateGenres(genres): state.selectedGenres = genres - UserDefaults.standard.genres = genres return .none - case let .updateWiFi(hasWiFi): - state.hasWiFi = hasWiFi - UserDefaults.standard.wifi = hasWiFi + case let .updateWiFi(wifi): + state.wifi = wifi return .none - case let .updatePrivateRoom(hasPrivateRoom): - state.hasPrivateRoom = hasPrivateRoom - UserDefaults.standard.privateRoom = hasPrivateRoom + case let .updatePrivateRoom(privateRoom): + state.privateRoom = privateRoom return .none - case let .updateNonSmoking(isNonSmoking): - state.isNonSmoking = isNonSmoking - UserDefaults.standard.nonSmoking = isNonSmoking + case let .updateNonSmoking(nonSmoking): + state.nonSmoking = nonSmoking + return .none + + case let .updateParking(parking): + state.parking = parking + return .none + + case .applyFilters: + UserDefaults.standard.range = state.selectedRange + UserDefaults.standard.budgets = state.selectedBudgets + UserDefaults.standard.genres = state.selectedGenres + UserDefaults.standard.wifi = state.wifi + UserDefaults.standard.privateRoom = state.privateRoom + UserDefaults.standard.nonSmoking = state.nonSmoking + UserDefaults.standard.parking = state.parking return .none case .resetFilters: - state = State() + state.selectedRange = 3 + state.selectedBudgets = [] + state.selectedGenres = [] + state.wifi = 0 + state.privateRoom = 0 + state.nonSmoking = 0 + state.parking = 0 return .none } } diff --git a/HotSpot/Sources/Presentation/SearchFilter/SearchFilterView.swift b/HotSpot/Sources/Presentation/SearchFilter/SearchFilterView.swift index 86dd6ca..6d8ee23 100644 --- a/HotSpot/Sources/Presentation/SearchFilter/SearchFilterView.swift +++ b/HotSpot/Sources/Presentation/SearchFilter/SearchFilterView.swift @@ -8,62 +8,83 @@ struct SearchFilterView: View { var body: some View { WithViewStore(store, observe: { $0 }) { viewStore in - VStack(spacing: 16) { - RangeSection( - selectedRange: viewStore.selectedRange, - onRangeSelected: { range in - viewStore.send(.updateRange(range)) + VStack(spacing: 0) { + TopBarView( + leftSide: .left, + leftAction: { + coordinator?.pop() + }, + rightSide: .icon, + rightIcon: UIImage.icRefresh, + rightAction: { + viewStore.send(.resetFilters) } ) - BudgetSection( - selectedBudgets: viewStore.selectedBudgets, - onBudgetSelected: { budgetCode in - var updatedBudgets = viewStore.selectedBudgets - if updatedBudgets.contains(budgetCode) { - updatedBudgets.removeAll { $0 == budgetCode } - } else if updatedBudgets.count < 2 { - updatedBudgets.append(budgetCode) - } - viewStore.send(.updateBudgets(updatedBudgets)) + ScrollView { + VStack(spacing: 24) { + RangeSection( + selectedRange: viewStore.selectedRange, + onRangeSelected: { range in + viewStore.send(.updateRange(range)) + } + ) + + BudgetSection( + selectedBudgets: viewStore.selectedBudgets, + onBudgetSelected: { budgetCode in + var updatedBudgets = viewStore.selectedBudgets + if updatedBudgets.contains(budgetCode) { + updatedBudgets.removeAll { $0 == budgetCode } + } else if updatedBudgets.count < 2 { + updatedBudgets.append(budgetCode) + } + viewStore.send(.updateBudgets(updatedBudgets)) + } + ) + + GenreSection( + selectedGenres: viewStore.selectedGenres, + onGenreSelected: { genreCode in + var updatedGenres = viewStore.selectedGenres + if updatedGenres.contains(genreCode) { + updatedGenres.removeAll { $0 == genreCode } + } else { + updatedGenres.append(genreCode) + } + viewStore.send(.updateGenres(updatedGenres)) + } + ) + + FeaturesSection( + wifi: viewStore.wifi, + privateRoom: viewStore.privateRoom, + nonSmoking: viewStore.nonSmoking, + parking: viewStore.parking, + onWiFiTapped: { viewStore.send(.updateWiFi(viewStore.wifi == 0 ? 1 : 0)) }, + onPrivateRoomTapped: { viewStore.send(.updatePrivateRoom(viewStore.privateRoom == 0 ? 1 : 0)) }, + onNonSmokingTapped: { viewStore.send(.updateNonSmoking(viewStore.nonSmoking == 0 ? 1 : 0)) }, + onParkingTapped: { viewStore.send(.updateParking(viewStore.parking == 0 ? 1 : 0)) } + ) } - ) - - GenreSection( - selectedGenres: viewStore.selectedGenres, - onGenreSelected: { genreCode in - var updatedGenres = viewStore.selectedGenres - if updatedGenres.contains(genreCode) { - updatedGenres.removeAll { $0 == genreCode } - } else { - updatedGenres.append(genreCode) - } - viewStore.send(.updateGenres(updatedGenres)) - } - ) - - VStack(spacing: 8) { - Toggle("WiFi", isOn: viewStore.binding( - get: \.hasWiFi, - send: { .updateWiFi($0) } - )) - - Toggle("개인실", isOn: viewStore.binding( - get: \.hasPrivateRoom, - send: { .updatePrivateRoom($0) } - )) - - Toggle("금연", isOn: viewStore.binding( - get: \.isNonSmoking, - send: { .updateNonSmoking($0) } - )) + .padding(.vertical, 16) } - .padding(.horizontal) - Button("필터 초기화") { - viewStore.send(.resetFilters) + VStack(spacing: 0) { + Divider() + + Button { + viewStore.send(.applyFilters) + coordinator?.pop() + } label: { + Text("フィルターを適用") + .font(.system(size: 16, weight: .semibold)) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .frame(height: 56) + .background(Color.blue) + } } - .padding() } } } From 6ebd592db9d24d1655dc33a1cfa4707edb8fa4b3 Mon Sep 17 00:00:00 2001 From: coby5502 Date: Tue, 22 Apr 2025 14:02:33 +0900 Subject: [PATCH 20/32] [FEAT] set location --- .../Dependency/UserDefaults+Dependency.swift | 15 ++++++++++++++ .../Map/Component/MapRepresentableView.swift | 2 ++ .../Presentation/Search/SearchStore.swift | 20 +++++-------------- .../Presentation/Search/SearchView.swift | 1 - 4 files changed, 22 insertions(+), 16 deletions(-) diff --git a/HotSpot/Sources/Common/Dependency/UserDefaults+Dependency.swift b/HotSpot/Sources/Common/Dependency/UserDefaults+Dependency.swift index 92a82ad..176e72f 100644 --- a/HotSpot/Sources/Common/Dependency/UserDefaults+Dependency.swift +++ b/HotSpot/Sources/Common/Dependency/UserDefaults+Dependency.swift @@ -22,6 +22,7 @@ extension UserDefaults { case privateRoom = "privateRoom" case nonSmoking = "nonSmoking" case parking = "parking" + case location = "location" } var range: Int { @@ -29,6 +30,20 @@ extension UserDefaults { set { set(newValue, forKey: FilterKey.range.rawValue) } } + var location: MapCoordinate { + get { + let lat = double(forKey: "\(FilterKey.location.rawValue)_lat") + let lng = double(forKey: "\(FilterKey.location.rawValue)_lng") + return lat == 0 && lng == 0 + ? MapCoordinate(latitude: 34.6937, longitude: 135.5023) // Osaka coordinates + : MapCoordinate(latitude: lat, longitude: lng) + } + set { + set(newValue.latitude, forKey: "\(FilterKey.location.rawValue)_lat") + set(newValue.longitude, forKey: "\(FilterKey.location.rawValue)_lng") + } + } + var budgets: [String] { get { stringArray(forKey: FilterKey.budgets.rawValue) ?? [] } set { set(newValue, forKey: FilterKey.budgets.rawValue) } diff --git a/HotSpot/Sources/Presentation/Map/Component/MapRepresentableView.swift b/HotSpot/Sources/Presentation/Map/Component/MapRepresentableView.swift index ff865c3..0fe7897 100644 --- a/HotSpot/Sources/Presentation/Map/Component/MapRepresentableView.swift +++ b/HotSpot/Sources/Presentation/Map/Component/MapRepresentableView.swift @@ -35,6 +35,7 @@ struct MapRepresentableView: UIViewRepresentable { DispatchQueue.main.async { self.parent.region.wrappedValue = region + UserDefaults.standard.location = MapCoordinate(coordinate: location.coordinate) } locationManager.stopUpdatingLocation() @@ -50,6 +51,7 @@ struct MapRepresentableView: UIViewRepresentable { DispatchQueue.main.async { self.parent.region.wrappedValue = region + UserDefaults.standard.location = osakaCoordinate } locationManager.stopUpdatingLocation() diff --git a/HotSpot/Sources/Presentation/Search/SearchStore.swift b/HotSpot/Sources/Presentation/Search/SearchStore.swift index 7377039..1ba84f4 100644 --- a/HotSpot/Sources/Presentation/Search/SearchStore.swift +++ b/HotSpot/Sources/Presentation/Search/SearchStore.swift @@ -5,21 +5,18 @@ import ComposableArchitecture @Reducer struct SearchStore { @Dependency(\.shopRepository) var shopRepository - @Dependency(\.locationManager) var locationManager @Dependency(\.userDefaults) var userDefaults struct State: Equatable { var shops: [ShopModel] = [] var searchText: String = "" var error: ShopError? = nil - var currentLocation: MapCoordinate? var paginationState: PaginationState = .init() } enum Action { case onAppear case search(String) - case updateLocation(MapCoordinate) case updateShops([ShopModel]) case handleError(Error) case clearError @@ -32,15 +29,9 @@ struct SearchStore { switch action { case .onAppear: return .run { send in - if let location = await locationManager.requestLocation() { - await send(.updateLocation(MapCoordinate(coordinate: location.coordinate))) - } + await send(.search("")) } - case let .updateLocation(location): - state.currentLocation = location - return .none - case let .updateShops(shops): state.shops = shops return .none @@ -66,12 +57,11 @@ struct SearchStore { state.searchText = text state.paginationState.reset() - return .run { [state] send in + return .run { send in do { - let location = state.currentLocation ?? MapCoordinate(latitude: 34.6937, longitude: 135.5023) let request = ShopSearchRequestDTO( - lat: location.latitude, - lng: location.longitude, + lat: userDefaults.location.latitude, + lng: userDefaults.location.longitude, range: userDefaults.range, count: nil, keyword: text, @@ -104,7 +94,7 @@ struct SearchStore { case .loadMore: return .run { [state] send in do { - let location = state.currentLocation ?? MapCoordinate(latitude: 34.6937, longitude: 135.5023) + let location = userDefaults.location let request = ShopSearchRequestDTO( lat: location.latitude, lng: location.longitude, diff --git a/HotSpot/Sources/Presentation/Search/SearchView.swift b/HotSpot/Sources/Presentation/Search/SearchView.swift index 16bcd4d..60156af 100644 --- a/HotSpot/Sources/Presentation/Search/SearchView.swift +++ b/HotSpot/Sources/Presentation/Search/SearchView.swift @@ -1,5 +1,4 @@ import SwiftUI -import CoreLocation import CobyDS import ComposableArchitecture From cdefe123116e84e5c1761a37cc70d2ff003d5e55 Mon Sep 17 00:00:00 2001 From: coby5502 Date: Tue, 22 Apr 2025 14:19:38 +0900 Subject: [PATCH 21/32] [FEAT] add data --- .../Sources/Data/DTO/Response/ShopDTO.swift | 36 ++++++++++++------- HotSpot/Sources/Domain/Model/ShopModel.swift | 10 ++++-- .../Map/Component/MapRepresentableView.swift | 2 +- .../Component/ShopInfoSection.swift | 34 +++++++++--------- .../Component/ShopLocationMapView.swift | 26 +++++++------- .../ShopDetail/ShopDetailView.swift | 8 ++++- 6 files changed, 70 insertions(+), 46 deletions(-) diff --git a/HotSpot/Sources/Data/DTO/Response/ShopDTO.swift b/HotSpot/Sources/Data/DTO/Response/ShopDTO.swift index f07afaa..22b891e 100644 --- a/HotSpot/Sources/Data/DTO/Response/ShopDTO.swift +++ b/HotSpot/Sources/Data/DTO/Response/ShopDTO.swift @@ -3,22 +3,19 @@ import Foundation struct ShopDTO: Decodable { let id: String let name: String - let nameKana: String? let address: String - let stationName: String? let lat: Double let lng: Double let access: String let open: String? - let close: String? let photo: Photo let genre: Genre - let catchPhrase: String let budget: Budget? let wifi: String? let nonSmoking: String? let privateRoom: String? - let card: String? + let parking: String? + let urls: Urls struct Photo: Decodable { let pc: PcPhoto @@ -46,33 +43,42 @@ struct ShopDTO: Decodable { case name case catchPhrase = "catch" } + + static func from(code: String) -> HotSpot.Genre? { + return HotSpot.Genre(rawValue: code) + } } struct Budget: Decodable { let code: String let name: String let average: String? + + static func from(code: String) -> HotSpot.Budget? { + return HotSpot.Budget(rawValue: code) + } + } + + struct Urls: Decodable { + let pc: String } enum CodingKeys: String, CodingKey { case id case name - case nameKana = "name_kana" case address - case stationName = "station_name" case lat case lng case access case open - case close case photo case genre - case catchPhrase = "catch" case budget case wifi case nonSmoking = "non_smoking" case privateRoom = "private_room" - case card + case parking + case urls } func toDomain() -> ShopModel { @@ -84,8 +90,14 @@ struct ShopDTO: Decodable { longitude: lng, imageUrl: photo.pc.large, access: access, - openingHours: open, - genreCode: genre.code + openingHours: open ?? "営業時間情報なし", + genre: Genre.from(code: genre.code) ?? .other, + budget: budget.flatMap { Budget.from(code: $0.code) } ?? .from1501to2000, + url: urls.pc, + wifi: wifi == "あり" ? 1 : 0, + privateRoom: privateRoom == "あり" ? 1 : 0, + nonSmoking: nonSmoking == "あり" ? 1 : 0, + parking: parking == "あり" ? 1 : 0 ) } } diff --git a/HotSpot/Sources/Domain/Model/ShopModel.swift b/HotSpot/Sources/Domain/Model/ShopModel.swift index 8534323..da18180 100644 --- a/HotSpot/Sources/Domain/Model/ShopModel.swift +++ b/HotSpot/Sources/Domain/Model/ShopModel.swift @@ -8,8 +8,14 @@ struct ShopModel: Identifiable, Equatable { let longitude: Double let imageUrl: String let access: String - let openingHours: String? - let genreCode: String + let openingHours: String + let genre: Genre + let budget: Budget + let url: String + let wifi: Int + let privateRoom: Int + let nonSmoking: Int + let parking: Int var coordinate: MapCoordinate { MapCoordinate(latitude: latitude, longitude: longitude) diff --git a/HotSpot/Sources/Presentation/Map/Component/MapRepresentableView.swift b/HotSpot/Sources/Presentation/Map/Component/MapRepresentableView.swift index 0fe7897..6957216 100644 --- a/HotSpot/Sources/Presentation/Map/Component/MapRepresentableView.swift +++ b/HotSpot/Sources/Presentation/Map/Component/MapRepresentableView.swift @@ -108,7 +108,7 @@ struct MapRepresentableView: UIViewRepresentable { coordinate: $0.coordinate.clLocationCoordinate2D, title: $0.name, shopId: $0.id, - genreCode: $0.genreCode + genreCode: $0.genre.rawValue ) } diff --git a/HotSpot/Sources/Presentation/ShopDetail/Component/ShopInfoSection.swift b/HotSpot/Sources/Presentation/ShopDetail/Component/ShopInfoSection.swift index 4b40735..2a05d9a 100644 --- a/HotSpot/Sources/Presentation/ShopDetail/Component/ShopInfoSection.swift +++ b/HotSpot/Sources/Presentation/ShopDetail/Component/ShopInfoSection.swift @@ -13,11 +13,9 @@ struct ShopInfoSection: View { .font(.pretendard(size: 24, weight: .bold)) .foregroundColor(Color.labelNormal) - if let genre = Genre.from(code: shop.genreCode) { - Text(genre.name) - .font(.pretendard(size: 14, weight: .regular)) - .foregroundColor(Color.labelAlternative) - } + Text(shop.genre.name) + .font(.pretendard(size: 14, weight: .regular)) + .foregroundColor(Color.labelAlternative) } // Address @@ -43,16 +41,14 @@ struct ShopInfoSection: View { } // Open Hours - if let openingHours = shop.openingHours { - VStack(alignment: .leading, spacing: 4) { - Text("営業時間") - .font(.pretendard(size: 16, weight: .semibold)) - .foregroundColor(Color.labelNormal) - - Text(openingHours) - .font(.pretendard(size: 16, weight: .regular)) - .foregroundColor(Color.labelNeutral) - } + VStack(alignment: .leading, spacing: 4) { + Text("営業時間") + .font(.pretendard(size: 16, weight: .semibold)) + .foregroundColor(Color.labelNormal) + + Text(shop.openingHours) + .font(.pretendard(size: 16, weight: .regular)) + .foregroundColor(Color.labelNeutral) } // Location @@ -81,7 +77,13 @@ struct ShopInfoSection: View { imageUrl: "https://example.com/image.jpg", access: "渋谷駅から徒歩5分", openingHours: "11:00-23:00", - genreCode: "G001" + genre: .izakaya, + budget: .from1501to2000, + url: "https://example.com", + wifi: 1, + privateRoom: 1, + nonSmoking: 1, + parking: 1 ) ) } diff --git a/HotSpot/Sources/Presentation/ShopDetail/Component/ShopLocationMapView.swift b/HotSpot/Sources/Presentation/ShopDetail/Component/ShopLocationMapView.swift index 818efd4..5469c15 100644 --- a/HotSpot/Sources/Presentation/ShopDetail/Component/ShopLocationMapView.swift +++ b/HotSpot/Sources/Presentation/ShopDetail/Component/ShopLocationMapView.swift @@ -17,19 +17,17 @@ struct ShopLocationMapView: View { Map(coordinateRegion: .constant(region), annotationItems: [ShopPin(shop: shop)]) { pin in MapAnnotation(coordinate: pin.coordinate) { - if let genre = Genre.from(code: pin.genreCode) { - ZStack { - Circle() - .fill(Color(uiColor: genre.color)) - .frame(width: 40, height: 40) + ZStack { + Circle() + .fill(Color(uiColor: pin.genre.color)) + .frame(width: 40, height: 40) - Image(uiImage: genre.image ?? UIImage()) - .resizable() - .renderingMode(.template) - .foregroundColor(.white) - .scaledToFit() - .frame(width: 24, height: 24) - } + Image(uiImage: pin.genre.image ?? UIImage()) + .resizable() + .renderingMode(.template) + .foregroundColor(.white) + .scaledToFit() + .frame(width: 24, height: 24) } } } @@ -47,10 +45,10 @@ struct ShopLocationMapView: View { private struct ShopPin: Identifiable { let id = UUID() let coordinate: CLLocationCoordinate2D - let genreCode: String + let genre: Genre init(shop: ShopModel) { self.coordinate = CLLocationCoordinate2D(latitude: shop.latitude, longitude: shop.longitude) - self.genreCode = shop.genreCode + self.genre = shop.genre } } diff --git a/HotSpot/Sources/Presentation/ShopDetail/ShopDetailView.swift b/HotSpot/Sources/Presentation/ShopDetail/ShopDetailView.swift index 10a5946..b1687a1 100644 --- a/HotSpot/Sources/Presentation/ShopDetail/ShopDetailView.swift +++ b/HotSpot/Sources/Presentation/ShopDetail/ShopDetailView.swift @@ -44,7 +44,13 @@ struct ShopDetailView: View { imageUrl: "https://example.com/image.jpg", access: "渋谷駅から徒歩5分", openingHours: "11:00-23:00", - genreCode: "G001" + genre: .izakaya, + budget: .from1501to2000, + url: "https://example.com", + wifi: 1, + privateRoom: 1, + nonSmoking: 1, + parking: 1 ) ), reducer: { ShopDetailStore() } From 3fbf07e1bd5a5e0fa20ce93f0af18840a0ebe69f Mon Sep 17 00:00:00 2001 From: coby5502 Date: Tue, 22 Apr 2025 18:55:35 +0900 Subject: [PATCH 22/32] [FEAT] complete detail view --- .../ShopDetail/Component/FacilityIcon.swift | 58 ++++++++ .../ShopDetail/Component/InfoRow.swift | 26 ++++ .../Component/ShopDetailSection.swift | 126 ++++++++++++++++++ .../Component/ShopInfoSection.swift | 89 ------------- .../ShopDetail/ShopDetailView.swift | 29 ++-- 5 files changed, 224 insertions(+), 104 deletions(-) create mode 100644 HotSpot/Sources/Presentation/ShopDetail/Component/FacilityIcon.swift create mode 100644 HotSpot/Sources/Presentation/ShopDetail/Component/InfoRow.swift create mode 100644 HotSpot/Sources/Presentation/ShopDetail/Component/ShopDetailSection.swift delete mode 100644 HotSpot/Sources/Presentation/ShopDetail/Component/ShopInfoSection.swift diff --git a/HotSpot/Sources/Presentation/ShopDetail/Component/FacilityIcon.swift b/HotSpot/Sources/Presentation/ShopDetail/Component/FacilityIcon.swift new file mode 100644 index 0000000..908dbc6 --- /dev/null +++ b/HotSpot/Sources/Presentation/ShopDetail/Component/FacilityIcon.swift @@ -0,0 +1,58 @@ +import SwiftUI +import CobyDS + +struct FacilityIcon: View { + let systemName: String + let title: String + let isAvailable: Bool + + var body: some View { + VStack(spacing: 8) { + ZStack { + Circle() + .fill(isAvailable ? Color.labelNormal.opacity(0.1) : Color.labelAlternative.opacity(0.1)) + .frame(width: 56, height: 56) + + if isAvailable { + Image(systemName: systemName) + .font(.system(size: 28)) + .foregroundColor(Color.labelNormal) + } else { + ZStack { + Image(systemName: systemName) + .font(.system(size: 28)) + .foregroundColor(Color.labelAlternative) + + Rectangle() + .fill(Color.labelAlternative) + .frame(width: 36, height: 2) + .rotationEffect(.degrees(45)) + } + } + } + + Text(title) + .font(.pretendard(size: 12, weight: .regular)) + .foregroundColor(isAvailable ? Color.labelNormal : Color.labelAlternative) + } + .padding(.horizontal, 4) + } +} + +#Preview { + HStack(spacing: 0) { + FacilityIcon( + systemName: "wifi", + title: "Wi-Fi", + isAvailable: true + ) + .frame(maxWidth: .infinity) + + FacilityIcon( + systemName: "door.left.hand.open", + title: "個室", + isAvailable: false + ) + .frame(maxWidth: .infinity) + } +} \ No newline at end of file diff --git a/HotSpot/Sources/Presentation/ShopDetail/Component/InfoRow.swift b/HotSpot/Sources/Presentation/ShopDetail/Component/InfoRow.swift new file mode 100644 index 0000000..ea83daf --- /dev/null +++ b/HotSpot/Sources/Presentation/ShopDetail/Component/InfoRow.swift @@ -0,0 +1,26 @@ +import SwiftUI +import CobyDS + +struct InfoRow: View { + let title: String + let content: String + + var body: some View { + VStack(alignment: .leading, spacing: 6) { + Text(title) + .font(.pretendard(size: 16, weight: .semibold)) + .foregroundColor(Color.labelNormal) + + Text(content) + .font(.pretendard(size: 16, weight: .regular)) + .foregroundColor(Color.labelNeutral) + } + } +} + +#Preview { + InfoRow( + title: "予算", + content: "¥1,501〜¥2,000" + ) +} \ No newline at end of file diff --git a/HotSpot/Sources/Presentation/ShopDetail/Component/ShopDetailSection.swift b/HotSpot/Sources/Presentation/ShopDetail/Component/ShopDetailSection.swift new file mode 100644 index 0000000..c02dbe1 --- /dev/null +++ b/HotSpot/Sources/Presentation/ShopDetail/Component/ShopDetailSection.swift @@ -0,0 +1,126 @@ +import SwiftUI +import CobyDS + +struct ShopDetailSection: View { + let shop: ShopModel + + var body: some View { + VStack(alignment: .leading, spacing: 32) { + // Basic Info + VStack(alignment: .leading, spacing: 12) { + Text(shop.name) + .font(.pretendard(size: 24, weight: .bold)) + .foregroundColor(Color.labelNormal) + + Text(shop.genre.name) + .font(.pretendard(size: 14, weight: .regular)) + .foregroundColor(Color.labelAlternative) + } + + // Facilities Grid + VStack(alignment: .leading, spacing: 16) { + Text("設備") + .font(.pretendard(size: 16, weight: .semibold)) + .foregroundColor(Color.labelNormal) + + HStack(spacing: 0) { + FacilityIcon( + systemName: "wifi", + title: "Wi-Fi", + isAvailable: shop.wifi == 1 + ) + .frame(maxWidth: .infinity) + + FacilityIcon( + systemName: "door.left.hand.open", + title: "個室", + isAvailable: shop.privateRoom == 1 + ) + .frame(maxWidth: .infinity) + + FacilityIcon( + systemName: "chair", + title: "禁煙席", + isAvailable: shop.nonSmoking == 1 + ) + .frame(maxWidth: .infinity) + + FacilityIcon( + systemName: "car", + title: "駐車場", + isAvailable: shop.parking == 1 + ) + .frame(maxWidth: .infinity) + } + } + + // Business Info + VStack(alignment: .leading, spacing: 20) { + // Budget + InfoRow(title: "予算", content: shop.budget.name) + + // Open Hours + InfoRow(title: "営業時間", content: shop.openingHours) + + // Access + InfoRow(title: "アクセス", content: shop.address) + + // Address + InfoRow(title: "住所", content: shop.access) + } + + // URL Button + if let url = URL(string: shop.url) { + Button(action: { + UIApplication.shared.open(url) + }) { + HStack(spacing: 8) { + Image(systemName: "link") + .font(.system(size: 16)) + Text("ウェブサイトを見る") + .font(.pretendard(size: 16, weight: .medium)) + } + .foregroundColor(Color.labelNormal) + .frame(maxWidth: .infinity) + .padding(.vertical, 12) + .background(Color.fillNormal) + .cornerRadius(8) + } + } + + // Location Map + VStack(alignment: .leading, spacing: 12) { + Text("位置情報") + .font(.pretendard(size: 16, weight: .semibold)) + .foregroundColor(Color.labelNormal) + + ShopLocationMapView(shop: shop) + .disabled(true) + } + } + .padding(.horizontal, BaseSize.horizantalPadding) + .padding(.vertical, BaseSize.verticalPadding) + } +} + +#Preview { + ShopDetailSection( + shop: ShopModel( + id: "test", + name: "テスト店舗", + address: "東京都渋谷区", + latitude: 35.6762, + longitude: 139.6503, + imageUrl: "https://example.com/image.jpg", + access: "渋谷駅から徒歩5分", + openingHours: "11:00-23:00", + genre: .izakaya, + budget: .from1501to2000, + url: "https://example.com", + wifi: 1, + privateRoom: 1, + nonSmoking: 1, + parking: 1 + ) + ) +} diff --git a/HotSpot/Sources/Presentation/ShopDetail/Component/ShopInfoSection.swift b/HotSpot/Sources/Presentation/ShopDetail/Component/ShopInfoSection.swift deleted file mode 100644 index 2a05d9a..0000000 --- a/HotSpot/Sources/Presentation/ShopDetail/Component/ShopInfoSection.swift +++ /dev/null @@ -1,89 +0,0 @@ -import SwiftUI -import CobyDS - -struct ShopInfoSection: View { - let shop: ShopModel - - var body: some View { - Group { - VStack(alignment: .leading, spacing: 24) { - // Name and Genre - VStack(alignment: .leading, spacing: 8) { - Text(shop.name) - .font(.pretendard(size: 24, weight: .bold)) - .foregroundColor(Color.labelNormal) - - Text(shop.genre.name) - .font(.pretendard(size: 14, weight: .regular)) - .foregroundColor(Color.labelAlternative) - } - - // Address - VStack(alignment: .leading, spacing: 4) { - Text("住所") - .font(.pretendard(size: 16, weight: .semibold)) - .foregroundColor(Color.labelNormal) - - Text(shop.address) - .font(.pretendard(size: 16, weight: .regular)) - .foregroundColor(Color.labelNeutral) - } - - // Access - VStack(alignment: .leading, spacing: 4) { - Text("アクセス") - .font(.pretendard(size: 16, weight: .semibold)) - .foregroundColor(Color.labelNormal) - - Text(shop.access) - .font(.pretendard(size: 16, weight: .regular)) - .foregroundColor(Color.labelNeutral) - } - - // Open Hours - VStack(alignment: .leading, spacing: 4) { - Text("営業時間") - .font(.pretendard(size: 16, weight: .semibold)) - .foregroundColor(Color.labelNormal) - - Text(shop.openingHours) - .font(.pretendard(size: 16, weight: .regular)) - .foregroundColor(Color.labelNeutral) - } - - // Location - VStack(alignment: .leading, spacing: 8) { - Text("位置情報") - .font(.pretendard(size: 16, weight: .semibold)) - .foregroundColor(Color.labelNormal) - - ShopLocationMapView(shop: shop) - } - } - .padding(.horizontal, BaseSize.horizantalPadding) - .padding(.vertical, BaseSize.verticalPadding) - } - } -} - -#Preview { - ShopInfoSection( - shop: ShopModel( - id: "test", - name: "テスト店舗", - address: "東京都渋谷区", - latitude: 35.6762, - longitude: 139.6503, - imageUrl: "https://example.com/image.jpg", - access: "渋谷駅から徒歩5分", - openingHours: "11:00-23:00", - genre: .izakaya, - budget: .from1501to2000, - url: "https://example.com", - wifi: 1, - privateRoom: 1, - nonSmoking: 1, - parking: 1 - ) - ) -} diff --git a/HotSpot/Sources/Presentation/ShopDetail/ShopDetailView.swift b/HotSpot/Sources/Presentation/ShopDetail/ShopDetailView.swift index b1687a1..9af5500 100644 --- a/HotSpot/Sources/Presentation/ShopDetail/ShopDetailView.swift +++ b/HotSpot/Sources/Presentation/ShopDetail/ShopDetailView.swift @@ -9,22 +9,21 @@ struct ShopDetailView: View { var body: some View { WithViewStore(store, observe: { $0 }) { viewStore in - Group { - VStack(spacing: 0) { - TopBarView( - leftSide: .left, - leftAction: { - coordinator?.pop() - } - ) - - ScrollView(showsIndicators: false) { - VStack(alignment: .leading, spacing: 8) { - ShopImageSection(imageUrl: viewStore.shop.imageUrl) - - ShopInfoSection(shop: viewStore.shop) - } + VStack(spacing: 0) { + TopBarView( + leftSide: .left, + leftAction: { + coordinator?.pop() } + ) + + ScrollView(showsIndicators: false) { + VStack(alignment: .leading, spacing: 8) { + ShopImageSection(imageUrl: viewStore.shop.imageUrl) + + ShopDetailSection(shop: viewStore.shop) + } + .padding(.bottom, BaseSize.verticalPadding) } } } From fd066762cb3e9fc27ac20c04b02a7a6579fbee9b Mon Sep 17 00:00:00 2001 From: coby5502 Date: Tue, 22 Apr 2025 19:28:54 +0900 Subject: [PATCH 23/32] [CHOER] fix button style --- .../DTO/Request/ShopSearchRequestDTO.swift | 4 +-- .../UseCase/InfiniteScrollSearchUseCase.swift | 2 +- .../Sources/Domain/UseCase/ShopsUseCase.swift | 2 +- .../Presentation/Search/SearchStore.swift | 4 +-- .../Component/BudgetSection.swift | 10 +++--- .../Component/DistanceSection.swift | 33 ------------------- .../Component/FeaturesSection.swift | 7 ++-- .../SearchFilter/Component/GenreSection.swift | 8 +++-- .../SearchFilter/Component/RangeSection.swift | 10 +++--- .../SearchFilter/SearchFilterView.swift | 25 +++++++------- .../Component/ShopDetailSection.swift | 18 +++++----- .../ShopDetail/ShopDetailView.swift | 1 + 12 files changed, 48 insertions(+), 76 deletions(-) delete mode 100644 HotSpot/Sources/Presentation/SearchFilter/Component/DistanceSection.swift diff --git a/HotSpot/Sources/Data/DTO/Request/ShopSearchRequestDTO.swift b/HotSpot/Sources/Data/DTO/Request/ShopSearchRequestDTO.swift index a04c472..7f91415 100644 --- a/HotSpot/Sources/Data/DTO/Request/ShopSearchRequestDTO.swift +++ b/HotSpot/Sources/Data/DTO/Request/ShopSearchRequestDTO.swift @@ -5,7 +5,7 @@ struct ShopSearchRequestDTO { let lng: Double // Longitude let range: Int // Search range (1–5) let count: Int? // Number of results (1–100) - let keyword: String? // Keyword search + let name: String? // Name search let genres: [String]? // Genre codes let start: Int? // Starting index for paging let budgets: [String]? // Budget codes @@ -27,7 +27,7 @@ struct ShopSearchRequestDTO { ] if let count = count { params["count"] = count } - if let keyword = keyword { params["keyword"] = keyword } + if let name = name { params["name"] = name } if let genres = genres, !genres.isEmpty { params["genre"] = genres.joined(separator: ",") } if let start = start { params["start"] = start } if let budgets = budgets, !budgets.isEmpty { params["budget"] = budgets.joined(separator: ",") } diff --git a/HotSpot/Sources/Domain/UseCase/InfiniteScrollSearchUseCase.swift b/HotSpot/Sources/Domain/UseCase/InfiniteScrollSearchUseCase.swift index 769f45d..498907a 100644 --- a/HotSpot/Sources/Domain/UseCase/InfiniteScrollSearchUseCase.swift +++ b/HotSpot/Sources/Domain/UseCase/InfiniteScrollSearchUseCase.swift @@ -22,7 +22,7 @@ struct InfiniteScrollSearchUseCase { lng: request.lng, range: request.range, count: pageSize, - keyword: request.keyword, + name: request.name, genres: request.genres, start: start, budgets: request.budgets, diff --git a/HotSpot/Sources/Domain/UseCase/ShopsUseCase.swift b/HotSpot/Sources/Domain/UseCase/ShopsUseCase.swift index 1e17345..7ffc2b9 100644 --- a/HotSpot/Sources/Domain/UseCase/ShopsUseCase.swift +++ b/HotSpot/Sources/Domain/UseCase/ShopsUseCase.swift @@ -13,7 +13,7 @@ struct ShopsUseCase { lng: lng, range: 5, count: nil, - keyword: nil, + name: nil, genres: nil, start: nil, budgets: nil, diff --git a/HotSpot/Sources/Presentation/Search/SearchStore.swift b/HotSpot/Sources/Presentation/Search/SearchStore.swift index 1ba84f4..d72434d 100644 --- a/HotSpot/Sources/Presentation/Search/SearchStore.swift +++ b/HotSpot/Sources/Presentation/Search/SearchStore.swift @@ -64,7 +64,7 @@ struct SearchStore { lng: userDefaults.location.longitude, range: userDefaults.range, count: nil, - keyword: text, + name: text, genres: !userDefaults.genres.isEmpty ? userDefaults.genres : nil, start: nil, budgets: !userDefaults.budgets.isEmpty ? userDefaults.budgets : nil, @@ -100,7 +100,7 @@ struct SearchStore { lng: location.longitude, range: userDefaults.range, count: nil, - keyword: state.searchText, + name: state.searchText, genres: !userDefaults.genres.isEmpty ? userDefaults.genres : nil, start: nil, budgets: !userDefaults.budgets.isEmpty ? userDefaults.budgets : nil, diff --git a/HotSpot/Sources/Presentation/SearchFilter/Component/BudgetSection.swift b/HotSpot/Sources/Presentation/SearchFilter/Component/BudgetSection.swift index 4be8dad..a666012 100644 --- a/HotSpot/Sources/Presentation/SearchFilter/Component/BudgetSection.swift +++ b/HotSpot/Sources/Presentation/SearchFilter/Component/BudgetSection.swift @@ -1,4 +1,5 @@ import SwiftUI +import CobyDS struct BudgetSection: View { let selectedBudgets: [String] @@ -7,8 +8,9 @@ struct BudgetSection: View { var body: some View { VStack(alignment: .leading, spacing: 12) { Text("予算") - .font(.system(size: 17, weight: .semibold)) - .padding(.horizontal, 16) + .font(.pretendard(size: 16, weight: .semibold)) + .foregroundColor(Color.labelNormal) + .padding(.horizontal, BaseSize.horizantalPadding) ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 8) { @@ -26,8 +28,8 @@ struct BudgetSection: View { } } } - .padding(.horizontal, 16) + .padding(.horizontal, BaseSize.horizantalPadding) } } } -} \ No newline at end of file +} diff --git a/HotSpot/Sources/Presentation/SearchFilter/Component/DistanceSection.swift b/HotSpot/Sources/Presentation/SearchFilter/Component/DistanceSection.swift deleted file mode 100644 index 139f5b0..0000000 --- a/HotSpot/Sources/Presentation/SearchFilter/Component/DistanceSection.swift +++ /dev/null @@ -1,33 +0,0 @@ -import SwiftUI - -struct DistanceSection: View { - let selectedDistance: String - let onDistanceSelected: (String) -> Void - - var body: some View { - VStack(alignment: .leading, spacing: 12) { - Text("거리") - .font(.system(size: 17, weight: .semibold)) - .padding(.horizontal, 16) - - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 8) { - ForEach(["1", "2", "3", "5", "10"], id: \.self) { distance in - Button { - onDistanceSelected(distance) - } label: { - Text("\(distance)km") - .font(.system(size: 14, weight: .medium)) - .padding(.horizontal, 16) - .padding(.vertical, 8) - .background(selectedDistance == distance ? Color.blue : Color(.systemGray6)) - .foregroundColor(selectedDistance == distance ? .white : .primary) - .cornerRadius(16) - } - } - } - .padding(.horizontal, 16) - } - } - } -} \ No newline at end of file diff --git a/HotSpot/Sources/Presentation/SearchFilter/Component/FeaturesSection.swift b/HotSpot/Sources/Presentation/SearchFilter/Component/FeaturesSection.swift index 0c6ca5d..447490a 100644 --- a/HotSpot/Sources/Presentation/SearchFilter/Component/FeaturesSection.swift +++ b/HotSpot/Sources/Presentation/SearchFilter/Component/FeaturesSection.swift @@ -14,7 +14,8 @@ struct FeaturesSection: View { var body: some View { VStack(alignment: .leading, spacing: 16) { Text("設備・サービス") - .font(.system(size: 16, weight: .semibold)) + .font(.pretendard(size: 16, weight: .semibold)) + .foregroundColor(Color.labelNormal) HStack(spacing: 8) { FeatureButton( @@ -42,7 +43,7 @@ struct FeaturesSection: View { ) } } - .padding(.horizontal, 16) + .padding(.horizontal, BaseSize.horizantalPadding) } } @@ -75,4 +76,4 @@ private struct FeatureButton: View { onNonSmokingTapped: {}, onParkingTapped: {} ) -} \ No newline at end of file +} diff --git a/HotSpot/Sources/Presentation/SearchFilter/Component/GenreSection.swift b/HotSpot/Sources/Presentation/SearchFilter/Component/GenreSection.swift index cb491fa..5ee2dbe 100644 --- a/HotSpot/Sources/Presentation/SearchFilter/Component/GenreSection.swift +++ b/HotSpot/Sources/Presentation/SearchFilter/Component/GenreSection.swift @@ -1,4 +1,5 @@ import SwiftUI +import CobyDS struct GenreSection: View { let selectedGenres: [String] @@ -7,8 +8,9 @@ struct GenreSection: View { var body: some View { VStack(alignment: .leading, spacing: 12) { Text("ジャンル") - .font(.system(size: 17, weight: .semibold)) - .padding(.horizontal, 16) + .font(.pretendard(size: 16, weight: .semibold)) + .foregroundColor(Color.labelNormal) + .padding(.horizontal, BaseSize.horizantalPadding) ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 8) { @@ -26,7 +28,7 @@ struct GenreSection: View { } } } - .padding(.horizontal, 16) + .padding(.horizontal, BaseSize.horizantalPadding) } } } diff --git a/HotSpot/Sources/Presentation/SearchFilter/Component/RangeSection.swift b/HotSpot/Sources/Presentation/SearchFilter/Component/RangeSection.swift index ccc3c9c..1c92dc0 100644 --- a/HotSpot/Sources/Presentation/SearchFilter/Component/RangeSection.swift +++ b/HotSpot/Sources/Presentation/SearchFilter/Component/RangeSection.swift @@ -1,4 +1,5 @@ import SwiftUI +import CobyDS struct RangeSection: View { let selectedRange: Int @@ -7,8 +8,9 @@ struct RangeSection: View { var body: some View { VStack(alignment: .leading, spacing: 12) { Text("距離") - .font(.system(size: 17, weight: .semibold)) - .padding(.horizontal, 16) + .font(.pretendard(size: 16, weight: .semibold)) + .foregroundColor(Color.labelNormal) + .padding(.horizontal, BaseSize.horizantalPadding) ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 8) { @@ -26,7 +28,7 @@ struct RangeSection: View { } } } - .padding(.horizontal, 16) + .padding(.horizontal, BaseSize.horizantalPadding) } } } @@ -41,4 +43,4 @@ struct RangeSection: View { default: return "1km" } } -} \ No newline at end of file +} diff --git a/HotSpot/Sources/Presentation/SearchFilter/SearchFilterView.swift b/HotSpot/Sources/Presentation/SearchFilter/SearchFilterView.swift index 6d8ee23..7ca6a61 100644 --- a/HotSpot/Sources/Presentation/SearchFilter/SearchFilterView.swift +++ b/HotSpot/Sources/Presentation/SearchFilter/SearchFilterView.swift @@ -70,22 +70,19 @@ struct SearchFilterView: View { .padding(.vertical, 16) } - VStack(spacing: 0) { - Divider() - - Button { - viewStore.send(.applyFilters) - coordinator?.pop() - } label: { - Text("フィルターを適用") - .font(.system(size: 16, weight: .semibold)) - .foregroundColor(.white) - .frame(maxWidth: .infinity) - .frame(height: 56) - .background(Color.blue) - } + Button { + viewStore.send(.applyFilters) + coordinator?.pop() + } label: { + Text("フィルターを適用") } + .buttonStyle( + CBButtonStyle() + ) + .padding(.horizontal, BaseSize.horizantalPadding) + .padding(.bottom, BaseSize.verticalPadding) } + .background(Color.backgroundNormalNormal) } } } diff --git a/HotSpot/Sources/Presentation/ShopDetail/Component/ShopDetailSection.swift b/HotSpot/Sources/Presentation/ShopDetail/Component/ShopDetailSection.swift index c02dbe1..f08fee7 100644 --- a/HotSpot/Sources/Presentation/ShopDetail/Component/ShopDetailSection.swift +++ b/HotSpot/Sources/Presentation/ShopDetail/Component/ShopDetailSection.swift @@ -71,21 +71,21 @@ struct ShopDetailSection: View { // URL Button if let url = URL(string: shop.url) { - Button(action: { + Button { UIApplication.shared.open(url) - }) { + } label: { HStack(spacing: 8) { Image(systemName: "link") - .font(.system(size: 16)) + Text("ウェブサイトを見る") - .font(.pretendard(size: 16, weight: .medium)) } - .foregroundColor(Color.labelNormal) - .frame(maxWidth: .infinity) - .padding(.vertical, 12) - .background(Color.fillNormal) - .cornerRadius(8) } + .buttonStyle( + CBButtonStyle( + buttonType: .outlined, + buttonSize: .medium + ) + ) } // Location Map diff --git a/HotSpot/Sources/Presentation/ShopDetail/ShopDetailView.swift b/HotSpot/Sources/Presentation/ShopDetail/ShopDetailView.swift index 9af5500..e99d77b 100644 --- a/HotSpot/Sources/Presentation/ShopDetail/ShopDetailView.swift +++ b/HotSpot/Sources/Presentation/ShopDetail/ShopDetailView.swift @@ -26,6 +26,7 @@ struct ShopDetailView: View { .padding(.bottom, BaseSize.verticalPadding) } } + .background(Color.backgroundNormalNormal) } } } From 432cdee681d5edcf83a15117ed83c019a4ebf3d8 Mon Sep 17 00:00:00 2001 From: coby5502 Date: Tue, 22 Apr 2025 19:42:20 +0900 Subject: [PATCH 24/32] [FEAT] seperate filter --- .../Sources/Domain/Model/Filter/Feature.swift | 12 +++ .../Sources/Domain/Model/Filter/Range.swift | 19 +++++ .../Component/BudgetSection.swift | 16 ++-- .../Component/FeaturesSection.swift | 79 +++++-------------- .../SearchFilter/Component/FilterButton.swift | 28 +++++++ .../SearchFilter/Component/RangeSection.swift | 29 ++----- .../SearchFilter/SearchFilterStore.swift | 59 ++++---------- .../SearchFilter/SearchFilterView.swift | 24 +++--- 8 files changed, 118 insertions(+), 148 deletions(-) create mode 100644 HotSpot/Sources/Domain/Model/Filter/Feature.swift create mode 100644 HotSpot/Sources/Domain/Model/Filter/Range.swift create mode 100644 HotSpot/Sources/Presentation/SearchFilter/Component/FilterButton.swift diff --git a/HotSpot/Sources/Domain/Model/Filter/Feature.swift b/HotSpot/Sources/Domain/Model/Filter/Feature.swift new file mode 100644 index 0000000..c2917ce --- /dev/null +++ b/HotSpot/Sources/Domain/Model/Filter/Feature.swift @@ -0,0 +1,12 @@ +import Foundation + +enum Feature: String, CaseIterable { + case wifi = "Wi-Fiあり" + case privateRoom = "個室あり" + case nonSmoking = "禁煙席あり" + case parking = "駐車場あり" + + var name: String { + return self.rawValue + } +} \ No newline at end of file diff --git a/HotSpot/Sources/Domain/Model/Filter/Range.swift b/HotSpot/Sources/Domain/Model/Filter/Range.swift new file mode 100644 index 0000000..f59cf64 --- /dev/null +++ b/HotSpot/Sources/Domain/Model/Filter/Range.swift @@ -0,0 +1,19 @@ +import Foundation + +enum Range: Int, CaseIterable { + case threeHundredMeters = 1 + case fiveHundredMeters = 2 + case oneKilometer = 3 + case twoKilometers = 4 + case threeKilometers = 5 + + var name: String { + switch self { + case .threeHundredMeters: return "300m" + case .fiveHundredMeters: return "500m" + case .oneKilometer: return "1km" + case .twoKilometers: return "2km" + case .threeKilometers: return "3km" + } + } +} \ No newline at end of file diff --git a/HotSpot/Sources/Presentation/SearchFilter/Component/BudgetSection.swift b/HotSpot/Sources/Presentation/SearchFilter/Component/BudgetSection.swift index a666012..0ce29e9 100644 --- a/HotSpot/Sources/Presentation/SearchFilter/Component/BudgetSection.swift +++ b/HotSpot/Sources/Presentation/SearchFilter/Component/BudgetSection.swift @@ -15,17 +15,11 @@ struct BudgetSection: View { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 8) { ForEach(Budget.allCases, id: \.self) { budget in - Button { - onBudgetSelected(budget.rawValue) - } label: { - Text(budget.name) - .font(.system(size: 14, weight: .medium)) - .padding(.horizontal, 16) - .padding(.vertical, 8) - .background(selectedBudgets.contains(budget.rawValue) ? Color.blue : Color(.systemGray6)) - .foregroundColor(selectedBudgets.contains(budget.rawValue) ? .white : .primary) - .cornerRadius(16) - } + FilterButton( + title: budget.name, + isSelected: selectedBudgets.contains(budget.rawValue), + action: { onBudgetSelected(budget.rawValue) } + ) } } .padding(.horizontal, BaseSize.horizantalPadding) diff --git a/HotSpot/Sources/Presentation/SearchFilter/Component/FeaturesSection.swift b/HotSpot/Sources/Presentation/SearchFilter/Component/FeaturesSection.swift index 447490a..580a224 100644 --- a/HotSpot/Sources/Presentation/SearchFilter/Component/FeaturesSection.swift +++ b/HotSpot/Sources/Presentation/SearchFilter/Component/FeaturesSection.swift @@ -2,78 +2,35 @@ import SwiftUI import CobyDS struct FeaturesSection: View { - let wifi: Int - let privateRoom: Int - let nonSmoking: Int - let parking: Int - let onWiFiTapped: () -> Void - let onPrivateRoomTapped: () -> Void - let onNonSmokingTapped: () -> Void - let onParkingTapped: () -> Void + let selectedFeatures: Set + let onFeatureSelected: (String) -> Void var body: some View { - VStack(alignment: .leading, spacing: 16) { - Text("設備・サービス") + VStack(alignment: .leading, spacing: 12) { + Text("特徴") .font(.pretendard(size: 16, weight: .semibold)) .foregroundColor(Color.labelNormal) + .padding(.horizontal, BaseSize.horizantalPadding) - HStack(spacing: 8) { - FeatureButton( - title: "Wi-Fiあり", - isSelected: wifi != 0, - action: onWiFiTapped - ) - - FeatureButton( - title: "個室あり", - isSelected: privateRoom != 0, - action: onPrivateRoomTapped - ) - - FeatureButton( - title: "禁煙席あり", - isSelected: nonSmoking != 0, - action: onNonSmokingTapped - ) - - FeatureButton( - title: "駐車場あり", - isSelected: parking != 0, - action: onParkingTapped - ) + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 8) { + ForEach(Feature.allCases, id: \.self) { feature in + FilterButton( + title: feature.name, + isSelected: selectedFeatures.contains(feature.rawValue), + action: { onFeatureSelected(feature.rawValue) } + ) + } + } + .padding(.horizontal, BaseSize.horizantalPadding) } } - .padding(.horizontal, BaseSize.horizantalPadding) - } -} - -private struct FeatureButton: View { - let title: String - let isSelected: Bool - let action: () -> Void - - var body: some View { - Button(action: action) { - Text(title) - .font(.system(size: 14)) - .foregroundColor(isSelected ? .white : .black) - .padding(.horizontal, 12) - .padding(.vertical, 8) - .background(isSelected ? Color.blue : Color(.systemGray6)) - .cornerRadius(16) - } } } #Preview { FeaturesSection( - wifi: 1, - privateRoom: 0, - nonSmoking: 1, - parking: 0, - onWiFiTapped: {}, - onPrivateRoomTapped: {}, - onNonSmokingTapped: {}, - onParkingTapped: {} + selectedFeatures: ["Wi-Fiあり", "個室あり", "禁煙席あり", "駐車場あり"], + onFeatureSelected: { _ in } ) } diff --git a/HotSpot/Sources/Presentation/SearchFilter/Component/FilterButton.swift b/HotSpot/Sources/Presentation/SearchFilter/Component/FilterButton.swift new file mode 100644 index 0000000..df8013c --- /dev/null +++ b/HotSpot/Sources/Presentation/SearchFilter/Component/FilterButton.swift @@ -0,0 +1,28 @@ +import SwiftUI +import CobyDS + +struct FilterButton: View { + let title: String + let isSelected: Bool + let action: () -> Void + + var body: some View { + Button(action: action) { + Text(title) + .font(.pretendard(size: 14, weight: .medium)) + .foregroundColor(isSelected ? .white : Color.labelNormal) + .padding(.horizontal, 16) + .padding(.vertical, 8) + .background(isSelected ? Color.primary : Color(.systemGray6)) + .cornerRadius(16) + } + } +} + +#Preview { + HStack(spacing: 8) { + FilterButton(title: "選択済み", isSelected: true, action: {}) + FilterButton(title: "未選択", isSelected: false, action: {}) + } + .padding() +} \ No newline at end of file diff --git a/HotSpot/Sources/Presentation/SearchFilter/Component/RangeSection.swift b/HotSpot/Sources/Presentation/SearchFilter/Component/RangeSection.swift index 1c92dc0..e426a9d 100644 --- a/HotSpot/Sources/Presentation/SearchFilter/Component/RangeSection.swift +++ b/HotSpot/Sources/Presentation/SearchFilter/Component/RangeSection.swift @@ -14,33 +14,16 @@ struct RangeSection: View { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 8) { - ForEach([1, 2, 3, 4, 5], id: \.self) { range in - Button { - onRangeSelected(range) - } label: { - Text(rangeText(for: range)) - .font(.system(size: 14, weight: .medium)) - .padding(.horizontal, 16) - .padding(.vertical, 8) - .background(selectedRange == range ? Color.blue : Color(.systemGray6)) - .foregroundColor(selectedRange == range ? .white : .primary) - .cornerRadius(16) - } + ForEach(Range.allCases, id: \.self) { range in + FilterButton( + title: range.name, + isSelected: selectedRange == range.rawValue, + action: { onRangeSelected(range.rawValue) } + ) } } .padding(.horizontal, BaseSize.horizantalPadding) } } } - - private func rangeText(for range: Int) -> String { - switch range { - case 1: return "300m" - case 2: return "500m" - case 3: return "1km" - case 4: return "2km" - case 5: return "3km" - default: return "1km" - } - } } diff --git a/HotSpot/Sources/Presentation/SearchFilter/SearchFilterStore.swift b/HotSpot/Sources/Presentation/SearchFilter/SearchFilterStore.swift index 46eada7..ac153d2 100644 --- a/HotSpot/Sources/Presentation/SearchFilter/SearchFilterStore.swift +++ b/HotSpot/Sources/Presentation/SearchFilter/SearchFilterStore.swift @@ -6,41 +6,29 @@ struct SearchFilterStore { @Dependency(\.userDefaults) var userDefaults struct State: Equatable { - var selectedRange: Int + var selectedRange: Range var selectedBudgets: [String] var selectedGenres: [String] - var wifi: Int - var privateRoom: Int - var nonSmoking: Int - var parking: Int + var selectedFeatures: Set init( - selectedRange: Int = UserDefaults.standard.range, + selectedRange: Range = Range(rawValue: UserDefaults.standard.range) ?? .oneKilometer, selectedBudgets: [String] = UserDefaults.standard.budgets, selectedGenres: [String] = UserDefaults.standard.genres, - wifi: Int = UserDefaults.standard.wifi, - privateRoom: Int = UserDefaults.standard.privateRoom, - nonSmoking: Int = UserDefaults.standard.nonSmoking, - parking: Int = UserDefaults.standard.parking + selectedFeatures: Set = [] ) { self.selectedRange = selectedRange self.selectedBudgets = selectedBudgets self.selectedGenres = selectedGenres - self.wifi = wifi - self.privateRoom = privateRoom - self.nonSmoking = nonSmoking - self.parking = parking + self.selectedFeatures = selectedFeatures } } enum Action: Equatable { - case updateRange(Int) + case updateRange(Range) case updateBudgets([String]) case updateGenres([String]) - case updateWiFi(Int) - case updatePrivateRoom(Int) - case updateNonSmoking(Int) - case updateParking(Int) + case updateFeatures(Set) case applyFilters case resetFilters } @@ -62,40 +50,25 @@ struct SearchFilterStore { state.selectedGenres = genres return .none - case let .updateWiFi(wifi): - state.wifi = wifi - return .none - - case let .updatePrivateRoom(privateRoom): - state.privateRoom = privateRoom - return .none - - case let .updateNonSmoking(nonSmoking): - state.nonSmoking = nonSmoking - return .none - - case let .updateParking(parking): - state.parking = parking + case let .updateFeatures(features): + state.selectedFeatures = features return .none case .applyFilters: - UserDefaults.standard.range = state.selectedRange + UserDefaults.standard.range = state.selectedRange.rawValue UserDefaults.standard.budgets = state.selectedBudgets UserDefaults.standard.genres = state.selectedGenres - UserDefaults.standard.wifi = state.wifi - UserDefaults.standard.privateRoom = state.privateRoom - UserDefaults.standard.nonSmoking = state.nonSmoking - UserDefaults.standard.parking = state.parking + UserDefaults.standard.wifi = state.selectedFeatures.contains("Wi-Fiあり") ? 1 : 0 + UserDefaults.standard.privateRoom = state.selectedFeatures.contains("個室あり") ? 1 : 0 + UserDefaults.standard.nonSmoking = state.selectedFeatures.contains("禁煙席あり") ? 1 : 0 + UserDefaults.standard.parking = state.selectedFeatures.contains("駐車場あり") ? 1 : 0 return .none case .resetFilters: - state.selectedRange = 3 + state.selectedRange = .oneKilometer state.selectedBudgets = [] state.selectedGenres = [] - state.wifi = 0 - state.privateRoom = 0 - state.nonSmoking = 0 - state.parking = 0 + state.selectedFeatures = [] return .none } } diff --git a/HotSpot/Sources/Presentation/SearchFilter/SearchFilterView.swift b/HotSpot/Sources/Presentation/SearchFilter/SearchFilterView.swift index 7ca6a61..2a5d642 100644 --- a/HotSpot/Sources/Presentation/SearchFilter/SearchFilterView.swift +++ b/HotSpot/Sources/Presentation/SearchFilter/SearchFilterView.swift @@ -24,9 +24,11 @@ struct SearchFilterView: View { ScrollView { VStack(spacing: 24) { RangeSection( - selectedRange: viewStore.selectedRange, + selectedRange: viewStore.selectedRange.rawValue, onRangeSelected: { range in - viewStore.send(.updateRange(range)) + if let range = Range(rawValue: range) { + viewStore.send(.updateRange(range)) + } } ) @@ -57,14 +59,16 @@ struct SearchFilterView: View { ) FeaturesSection( - wifi: viewStore.wifi, - privateRoom: viewStore.privateRoom, - nonSmoking: viewStore.nonSmoking, - parking: viewStore.parking, - onWiFiTapped: { viewStore.send(.updateWiFi(viewStore.wifi == 0 ? 1 : 0)) }, - onPrivateRoomTapped: { viewStore.send(.updatePrivateRoom(viewStore.privateRoom == 0 ? 1 : 0)) }, - onNonSmokingTapped: { viewStore.send(.updateNonSmoking(viewStore.nonSmoking == 0 ? 1 : 0)) }, - onParkingTapped: { viewStore.send(.updateParking(viewStore.parking == 0 ? 1 : 0)) } + selectedFeatures: viewStore.selectedFeatures, + onFeatureSelected: { feature in + var updatedFeatures = viewStore.selectedFeatures + if updatedFeatures.contains(feature) { + updatedFeatures.remove(feature) + } else { + updatedFeatures.insert(feature) + } + viewStore.send(.updateFeatures(updatedFeatures)) + } ) } .padding(.vertical, 16) From d571357dafd61c72f63d3c679c3d39fa4003f927 Mon Sep 17 00:00:00 2001 From: coby5502 Date: Tue, 22 Apr 2025 20:14:21 +0900 Subject: [PATCH 25/32] [CHORE] fix --- .../Search/Component/SearchBar.swift | 2 +- .../Search/Component/SearchResults.swift | 17 ++++++++--------- .../Presentation/Search/SearchView.swift | 6 +++--- .../Component/FeaturesSection.swift | 2 +- .../SearchFilter/Component/FilterButton.swift | 6 +++--- .../SearchFilter/Component/GenreSection.swift | 16 +++++----------- .../SearchFilter/SearchFilterView.swift | 4 +++- 7 files changed, 24 insertions(+), 29 deletions(-) diff --git a/HotSpot/Sources/Presentation/Search/Component/SearchBar.swift b/HotSpot/Sources/Presentation/Search/Component/SearchBar.swift index 9cbbaa0..4b43ae8 100644 --- a/HotSpot/Sources/Presentation/Search/Component/SearchBar.swift +++ b/HotSpot/Sources/Presentation/Search/Component/SearchBar.swift @@ -1,4 +1,5 @@ import SwiftUI +import CobyDS struct SearchBar: View { let searchText: String @@ -20,7 +21,6 @@ struct SearchBar: View { .padding(8) .background(Color(.systemGray6)) .cornerRadius(8) - .padding(.horizontal) .onChange(of: isFocused) { newValue in isSearchFocused = newValue } diff --git a/HotSpot/Sources/Presentation/Search/Component/SearchResults.swift b/HotSpot/Sources/Presentation/Search/Component/SearchResults.swift index 0b45ebf..d4cb679 100644 --- a/HotSpot/Sources/Presentation/Search/Component/SearchResults.swift +++ b/HotSpot/Sources/Presentation/Search/Component/SearchResults.swift @@ -15,8 +15,6 @@ struct SearchResults: View { if let error = error { Text(ShopErrorMessageMapper.message(for: error)) .foregroundColor(.red) - } else if searchText.isEmpty { - EmptyResults(searchText: searchText) } else if shops.isEmpty { EmptyResults(searchText: searchText) } else { @@ -42,17 +40,18 @@ struct SearchResults: View { } } } - .padding() + .padding(.horizontal, BaseSize.horizantalPadding) + .padding(.vertical, BaseSize.verticalPadding) } - .simultaneousGesture( - DragGesture(minimumDistance: 0) - .onChanged { _ in - UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) - } - ) } } .frame(maxWidth: .infinity, maxHeight: .infinity) + .simultaneousGesture( + DragGesture(minimumDistance: 0) + .onChanged { _ in + UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) + } + ) } private func loadImage(for shop: ShopModel) { diff --git a/HotSpot/Sources/Presentation/Search/SearchView.swift b/HotSpot/Sources/Presentation/Search/SearchView.swift index 60156af..5c5a21e 100644 --- a/HotSpot/Sources/Presentation/Search/SearchView.swift +++ b/HotSpot/Sources/Presentation/Search/SearchView.swift @@ -29,6 +29,8 @@ struct SearchView: View { onSearch: { viewStore.send(.search($0)) }, isSearchFocused: $isSearchFocused ) + .padding(.horizontal, BaseSize.horizantalPadding) + .padding(.vertical, isSearchFocused ? BaseSize.verticalPadding : 0) SearchResults( error: viewStore.error, @@ -42,6 +44,7 @@ struct SearchView: View { } ) } + .background(Color.backgroundNormalNormal) .onAppear { viewStore.send(.onAppear) } @@ -51,9 +54,6 @@ struct SearchView: View { viewStore.send(.clearError) } } - .onTapGesture { - UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) - } } } } diff --git a/HotSpot/Sources/Presentation/SearchFilter/Component/FeaturesSection.swift b/HotSpot/Sources/Presentation/SearchFilter/Component/FeaturesSection.swift index 580a224..0a1346d 100644 --- a/HotSpot/Sources/Presentation/SearchFilter/Component/FeaturesSection.swift +++ b/HotSpot/Sources/Presentation/SearchFilter/Component/FeaturesSection.swift @@ -7,7 +7,7 @@ struct FeaturesSection: View { var body: some View { VStack(alignment: .leading, spacing: 12) { - Text("特徴") + Text("設備") .font(.pretendard(size: 16, weight: .semibold)) .foregroundColor(Color.labelNormal) .padding(.horizontal, BaseSize.horizantalPadding) diff --git a/HotSpot/Sources/Presentation/SearchFilter/Component/FilterButton.swift b/HotSpot/Sources/Presentation/SearchFilter/Component/FilterButton.swift index df8013c..78c6e08 100644 --- a/HotSpot/Sources/Presentation/SearchFilter/Component/FilterButton.swift +++ b/HotSpot/Sources/Presentation/SearchFilter/Component/FilterButton.swift @@ -10,10 +10,10 @@ struct FilterButton: View { Button(action: action) { Text(title) .font(.pretendard(size: 14, weight: .medium)) - .foregroundColor(isSelected ? .white : Color.labelNormal) + .foregroundColor(isSelected ? Color.inverseLabel : Color.labelNormal) .padding(.horizontal, 16) .padding(.vertical, 8) - .background(isSelected ? Color.primary : Color(.systemGray6)) + .background(isSelected ? Color.labelNormal : Color.fillNormal) .cornerRadius(16) } } @@ -25,4 +25,4 @@ struct FilterButton: View { FilterButton(title: "未選択", isSelected: false, action: {}) } .padding() -} \ No newline at end of file +} diff --git a/HotSpot/Sources/Presentation/SearchFilter/Component/GenreSection.swift b/HotSpot/Sources/Presentation/SearchFilter/Component/GenreSection.swift index 5ee2dbe..8af19c7 100644 --- a/HotSpot/Sources/Presentation/SearchFilter/Component/GenreSection.swift +++ b/HotSpot/Sources/Presentation/SearchFilter/Component/GenreSection.swift @@ -15,17 +15,11 @@ struct GenreSection: View { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 8) { ForEach(Genre.allCases, id: \.self) { genre in - Button { - onGenreSelected(genre.rawValue) - } label: { - Text(genre.name) - .font(.system(size: 14, weight: .medium)) - .padding(.horizontal, 16) - .padding(.vertical, 8) - .background(selectedGenres.contains(genre.rawValue) ? Color.blue : Color(.systemGray6)) - .foregroundColor(selectedGenres.contains(genre.rawValue) ? .white : .primary) - .cornerRadius(16) - } + FilterButton( + title: genre.name, + isSelected: selectedGenres.contains(genre.rawValue), + action: { onGenreSelected(genre.rawValue) } + ) } } .padding(.horizontal, BaseSize.horizantalPadding) diff --git a/HotSpot/Sources/Presentation/SearchFilter/SearchFilterView.swift b/HotSpot/Sources/Presentation/SearchFilter/SearchFilterView.swift index 2a5d642..4827930 100644 --- a/HotSpot/Sources/Presentation/SearchFilter/SearchFilterView.swift +++ b/HotSpot/Sources/Presentation/SearchFilter/SearchFilterView.swift @@ -81,7 +81,9 @@ struct SearchFilterView: View { Text("フィルターを適用") } .buttonStyle( - CBButtonStyle() + CBButtonStyle( + buttonColor: Color.redOrangeNormal + ) ) .padding(.horizontal, BaseSize.horizantalPadding) .padding(.bottom, BaseSize.verticalPadding) From c8b30d955bd86bce87f3c6d52e838aea036bfba4 Mon Sep 17 00:00:00 2001 From: coby5502 Date: Tue, 22 Apr 2025 20:29:22 +0900 Subject: [PATCH 26/32] [FEAT] complete search design --- .../Search/Component/EmptyResults.swift | 15 ++++++---- .../Search/Component/SearchBar.swift | 28 +++++++++++++++---- .../Search/Component/SearchResults.swift | 5 ++-- .../Presentation/Search/SearchView.swift | 4 +-- 4 files changed, 36 insertions(+), 16 deletions(-) diff --git a/HotSpot/Sources/Presentation/Search/Component/EmptyResults.swift b/HotSpot/Sources/Presentation/Search/Component/EmptyResults.swift index 252a1be..90c1541 100644 --- a/HotSpot/Sources/Presentation/Search/Component/EmptyResults.swift +++ b/HotSpot/Sources/Presentation/Search/Component/EmptyResults.swift @@ -1,17 +1,20 @@ import SwiftUI +import CobyDS struct EmptyResults: View { let searchText: String var body: some View { VStack(spacing: 16) { - Image(systemName: "magnifyingglass") + Image(systemName: "exclamationmark.circle") .font(.system(size: 48)) - .foregroundColor(.gray) - Text(searchText.isEmpty ? "검색어를 입력해주세요" : "검색 결과가 없습니다") - .font(.headline) - .foregroundColor(.gray) + .foregroundColor(Color.labelAssistive) + + Text(searchText.isEmpty ? "店舗名で検索してください" : "検索結果が見つかりません") + .font(.pretendard(size: 16, weight: .medium)) + .foregroundColor(Color.labelAssistive) } .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color.backgroundNormalNormal) } -} \ No newline at end of file +} diff --git a/HotSpot/Sources/Presentation/Search/Component/SearchBar.swift b/HotSpot/Sources/Presentation/Search/Component/SearchBar.swift index 4b43ae8..dfe9095 100644 --- a/HotSpot/Sources/Presentation/Search/Component/SearchBar.swift +++ b/HotSpot/Sources/Presentation/Search/Component/SearchBar.swift @@ -9,23 +9,39 @@ struct SearchBar: View { var body: some View { HStack { - Image(systemName: "magnifyingglass") - .foregroundColor(.gray) - TextField("검색어를 입력하세요", text: .init( + Image(uiImage: UIImage.icSearch) + .foregroundColor(Color.labelAssistive) + + TextField("店舗名で検索", text: .init( get: { searchText }, set: { onSearch($0) } )) .textFieldStyle(.plain) .focused($isFocused) + + if isSearchFocused { + Button { + isFocused = false + } label: { + Text("キャンセル") + .font(.pretendard(size: 14, weight: .medium)) + .foregroundColor(Color.labelNeutral) + } + .transition(.opacity) + } } .padding(8) - .background(Color(.systemGray6)) + .background(Color.fillNormal) .cornerRadius(8) .onChange(of: isFocused) { newValue in - isSearchFocused = newValue + withAnimation(.easeInOut(duration: 0.3)) { + isSearchFocused = newValue + } } .onChange(of: isSearchFocused) { newValue in - isFocused = newValue + withAnimation(.easeInOut(duration: 0.3)) { + isFocused = newValue + } } } } diff --git a/HotSpot/Sources/Presentation/Search/Component/SearchResults.swift b/HotSpot/Sources/Presentation/Search/Component/SearchResults.swift index d4cb679..156b04a 100644 --- a/HotSpot/Sources/Presentation/Search/Component/SearchResults.swift +++ b/HotSpot/Sources/Presentation/Search/Component/SearchResults.swift @@ -19,7 +19,7 @@ struct SearchResults: View { EmptyResults(searchText: searchText) } else { ScrollView { - LazyVStack(spacing: 16) { + LazyVStack(spacing: BaseSize.cellVerticalSpacing) { ForEach(shops) { shop in ThumbnailTileView( image: $shopImages[shop.id], @@ -41,7 +41,8 @@ struct SearchResults: View { } } .padding(.horizontal, BaseSize.horizantalPadding) - .padding(.vertical, BaseSize.verticalPadding) + .padding(.top, 8) + .padding(.bottom, BaseSize.verticalPadding) } } } diff --git a/HotSpot/Sources/Presentation/Search/SearchView.swift b/HotSpot/Sources/Presentation/Search/SearchView.swift index 5c5a21e..8adccb2 100644 --- a/HotSpot/Sources/Presentation/Search/SearchView.swift +++ b/HotSpot/Sources/Presentation/Search/SearchView.swift @@ -9,7 +9,7 @@ struct SearchView: View { var body: some View { WithViewStore(store, observe: { $0 }) { viewStore in - VStack(spacing: 0) { + VStack(spacing: 8) { if !isSearchFocused { TopBarView( leftSide: .left, @@ -30,7 +30,7 @@ struct SearchView: View { isSearchFocused: $isSearchFocused ) .padding(.horizontal, BaseSize.horizantalPadding) - .padding(.vertical, isSearchFocused ? BaseSize.verticalPadding : 0) + .padding(.top, isSearchFocused ? BaseSize.verticalPadding : 0) SearchResults( error: viewStore.error, From 5eb13a496f82a1089e119f0e7857afd6e2162c15 Mon Sep 17 00:00:00 2001 From: coby5502 Date: Tue, 22 Apr 2025 20:40:13 +0900 Subject: [PATCH 27/32] [FEAT] complete all design --- .../Sources/Presentation/SearchFilter/SearchFilterView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HotSpot/Sources/Presentation/SearchFilter/SearchFilterView.swift b/HotSpot/Sources/Presentation/SearchFilter/SearchFilterView.swift index 4827930..6bded3d 100644 --- a/HotSpot/Sources/Presentation/SearchFilter/SearchFilterView.swift +++ b/HotSpot/Sources/Presentation/SearchFilter/SearchFilterView.swift @@ -82,7 +82,7 @@ struct SearchFilterView: View { } .buttonStyle( CBButtonStyle( - buttonColor: Color.redOrangeNormal + buttonColor: Color.limeNormal ) ) .padding(.horizontal, BaseSize.horizantalPadding) From b975f1f41025ac5c7e5c9bea7fa1698982d6400e Mon Sep 17 00:00:00 2001 From: coby5502 Date: Tue, 22 Apr 2025 20:48:27 +0900 Subject: [PATCH 28/32] [FEAT] init search --- .../Presentation/Search/SearchStore.swift | 16 +++++++--------- .../Sources/Presentation/Search/SearchView.swift | 2 +- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/HotSpot/Sources/Presentation/Search/SearchStore.swift b/HotSpot/Sources/Presentation/Search/SearchStore.swift index d72434d..d863428 100644 --- a/HotSpot/Sources/Presentation/Search/SearchStore.swift +++ b/HotSpot/Sources/Presentation/Search/SearchStore.swift @@ -12,10 +12,10 @@ struct SearchStore { var searchText: String = "" var error: ShopError? = nil var paginationState: PaginationState = .init() + var isInitialSearch: Bool = true } enum Action { - case onAppear case search(String) case updateShops([ShopModel]) case handleError(Error) @@ -27,11 +27,6 @@ struct SearchStore { var body: some ReducerOf { Reduce { state, action in switch action { - case .onAppear: - return .run { send in - await send(.search("")) - } - case let .updateShops(shops): state.shops = shops return .none @@ -52,10 +47,13 @@ struct SearchStore { return .none case let .search(text): - guard text != state.searchText else { return .none } + if !state.isInitialSearch && text == state.searchText { + return .none + } state.searchText = text state.paginationState.reset() + state.isInitialSearch = false return .run { send in do { @@ -64,7 +62,7 @@ struct SearchStore { lng: userDefaults.location.longitude, range: userDefaults.range, count: nil, - name: text, + name: text.isEmpty ? nil : text, genres: !userDefaults.genres.isEmpty ? userDefaults.genres : nil, start: nil, budgets: !userDefaults.budgets.isEmpty ? userDefaults.budgets : nil, @@ -100,7 +98,7 @@ struct SearchStore { lng: location.longitude, range: userDefaults.range, count: nil, - name: state.searchText, + name: state.searchText.isEmpty ? nil : state.searchText, genres: !userDefaults.genres.isEmpty ? userDefaults.genres : nil, start: nil, budgets: !userDefaults.budgets.isEmpty ? userDefaults.budgets : nil, diff --git a/HotSpot/Sources/Presentation/Search/SearchView.swift b/HotSpot/Sources/Presentation/Search/SearchView.swift index 8adccb2..c40b503 100644 --- a/HotSpot/Sources/Presentation/Search/SearchView.swift +++ b/HotSpot/Sources/Presentation/Search/SearchView.swift @@ -46,7 +46,7 @@ struct SearchView: View { } .background(Color.backgroundNormalNormal) .onAppear { - viewStore.send(.onAppear) + viewStore.send(.search("")) } .onChange(of: viewStore.error) { error in if let error = error { From aa2102fac0362ed121043de98ad33242f030a57e Mon Sep 17 00:00:00 2001 From: coby5502 Date: Tue, 22 Apr 2025 23:54:05 +0900 Subject: [PATCH 29/32] [CHORE] remove paging model --- .../Domain/Model/PaginationState.swift | 35 ------------------- .../Presentation/Search/SearchStore.swift | 29 +++++++-------- .../Presentation/Search/SearchView.swift | 2 +- .../Component/ShopDetailSection.swift | 7 +++- 4 files changed, 20 insertions(+), 53 deletions(-) delete mode 100644 HotSpot/Sources/Domain/Model/PaginationState.swift diff --git a/HotSpot/Sources/Domain/Model/PaginationState.swift b/HotSpot/Sources/Domain/Model/PaginationState.swift deleted file mode 100644 index 63ffbc4..0000000 --- a/HotSpot/Sources/Domain/Model/PaginationState.swift +++ /dev/null @@ -1,35 +0,0 @@ -import Foundation - -struct PaginationState: Equatable { - var currentPage: Int - var isLastPage: Bool - var isLoading: Bool - - init(currentPage: Int = 1, isLastPage: Bool = false, isLoading: Bool = false) { - self.currentPage = currentPage - self.isLastPage = isLastPage - self.isLoading = isLoading - } - - mutating func reset() { - currentPage = 1 - isLastPage = false - isLoading = false - } - - mutating func update(isLastPage: Bool) { - self.isLastPage = isLastPage - } - - mutating func startLoading() { - isLoading = true - } - - mutating func finishLoading() { - isLoading = false - } - - mutating func incrementPage() { - currentPage += 1 - } -} \ No newline at end of file diff --git a/HotSpot/Sources/Presentation/Search/SearchStore.swift b/HotSpot/Sources/Presentation/Search/SearchStore.swift index d863428..06b0cdd 100644 --- a/HotSpot/Sources/Presentation/Search/SearchStore.swift +++ b/HotSpot/Sources/Presentation/Search/SearchStore.swift @@ -11,7 +11,8 @@ struct SearchStore { var shops: [ShopModel] = [] var searchText: String = "" var error: ShopError? = nil - var paginationState: PaginationState = .init() + var currentPage: Int = 1 + var isLastPage: Bool = false var isInitialSearch: Bool = true } @@ -21,7 +22,7 @@ struct SearchStore { case handleError(Error) case clearError case loadMore - case updatePaginationState(PaginationState) + case updatePage(Int, Bool) } var body: some ReducerOf { @@ -52,7 +53,8 @@ struct SearchStore { } state.searchText = text - state.paginationState.reset() + state.currentPage = 1 + state.isLastPage = false state.isInitialSearch = false return .run { send in @@ -79,17 +81,15 @@ struct SearchStore { ) await send(.updateShops(result.shops)) - await send(.updatePaginationState(PaginationState( - currentPage: result.currentPage, - isLastPage: !result.hasMore, - isLoading: false - ))) + await send(.updatePage(result.currentPage, !result.hasMore)) } catch { await send(.handleError(error)) } } case .loadMore: + if state.isLastPage { return .none } + return .run { [state] send in do { let location = userDefaults.location @@ -111,7 +111,7 @@ struct SearchStore { let useCase = InfiniteScrollSearchUseCase(repository: shopRepository) let result = try await useCase.execute( request: request, - currentPage: state.paginationState.currentPage, + currentPage: state.currentPage, isLoadMore: true ) @@ -119,18 +119,15 @@ struct SearchStore { let newShops = result.shops.filter { !existingShopIds.contains($0.id) } await send(.updateShops(state.shops + newShops)) - await send(.updatePaginationState(PaginationState( - currentPage: result.currentPage, - isLastPage: !result.hasMore, - isLoading: false - ))) + await send(.updatePage(result.currentPage, !result.hasMore)) } catch { await send(.handleError(error)) } } - case let .updatePaginationState(paginationState): - state.paginationState = paginationState + case let .updatePage(page, isLastPage): + state.currentPage = page + state.isLastPage = isLastPage return .none } } diff --git a/HotSpot/Sources/Presentation/Search/SearchView.swift b/HotSpot/Sources/Presentation/Search/SearchView.swift index c40b503..99efa48 100644 --- a/HotSpot/Sources/Presentation/Search/SearchView.swift +++ b/HotSpot/Sources/Presentation/Search/SearchView.swift @@ -38,7 +38,7 @@ struct SearchView: View { shops: viewStore.shops, onSelectShop: { coordinator?.showShopDetail($0) }, onLoadMore: { - if !viewStore.paginationState.isLastPage { + if !viewStore.isLastPage { viewStore.send(.loadMore) } } diff --git a/HotSpot/Sources/Presentation/ShopDetail/Component/ShopDetailSection.swift b/HotSpot/Sources/Presentation/ShopDetail/Component/ShopDetailSection.swift index f08fee7..1b3d6bd 100644 --- a/HotSpot/Sources/Presentation/ShopDetail/Component/ShopDetailSection.swift +++ b/HotSpot/Sources/Presentation/ShopDetail/Component/ShopDetailSection.swift @@ -95,7 +95,12 @@ struct ShopDetailSection: View { .foregroundColor(Color.labelNormal) ShopLocationMapView(shop: shop) - .disabled(true) + .onTapGesture { + let urlString = "http://maps.apple.com/?ll=\(shop.latitude),\(shop.longitude)&q=\(shop.name)" + if let url = URL(string: urlString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "") { + UIApplication.shared.open(url) + } + } } } .padding(.horizontal, BaseSize.horizantalPadding) From 6b325235f72aa877dd1508d0b3019d0d218b5221 Mon Sep 17 00:00:00 2001 From: coby5502 Date: Wed, 23 Apr 2025 00:49:17 +0900 Subject: [PATCH 30/32] [FIX] fix icons --- .../Sources/Domain/Model/Filter/Genre.swift | 51 ++++++++++++------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/HotSpot/Sources/Domain/Model/Filter/Genre.swift b/HotSpot/Sources/Domain/Model/Filter/Genre.swift index 7e37adb..6250a90 100644 --- a/HotSpot/Sources/Domain/Model/Filter/Genre.swift +++ b/HotSpot/Sources/Domain/Model/Filter/Genre.swift @@ -65,23 +65,40 @@ public enum Genre: String, CaseIterable { public var image: UIImage? { switch self { - case .izakaya: return UIImage(systemName: "wineglass.fill") - case .diningBar: return UIImage(systemName: "wineglass") - case .creative: return UIImage(systemName: "fork.knife") - case .japanese: return UIImage(systemName: "leaf.fill") - case .western: return UIImage(systemName: "fork.knife.circle") - case .italianFrench: return UIImage(systemName: "fork.knife.circle.fill") - case .chinese: return UIImage(systemName: "bowl.fill") - case .yakiniku: return UIImage(systemName: "flame.fill") - case .asian: return UIImage(systemName: "globe.asia.australia.fill") - case .international: return UIImage(systemName: "globe") - case .karaoke: return UIImage(systemName: "music.mic") - case .bar: return UIImage(systemName: "wineglass") - case .ramen: return UIImage(systemName: "bowl") - case .cafe: return UIImage(systemName: "cup.and.saucer.fill") - case .okonomiyaki: return UIImage(systemName: "flame") - case .korean: return UIImage(systemName: "bowl") - case .other: return UIImage(systemName: "questionmark.circle.fill") + case .izakaya: + return UIImage(systemName: "wineglass.fill") + case .diningBar: + return UIImage(systemName: "fork.knife") + case .creative: + return UIImage(systemName: "sparkles") + case .japanese: + return UIImage(systemName: "leaf.fill") + case .western: + return UIImage(systemName: "fork.knife.circle") + case .italianFrench: + return UIImage(systemName: "fork.knife.circle.fill") + case .chinese: + return UIImage(systemName: "takeoutbag.and.cup.and.straw.fill") + case .yakiniku: + return UIImage(systemName: "flame.fill") + case .asian: + return UIImage(systemName: "globe.asia.australia") + case .international: + return UIImage(systemName: "globe") + case .karaoke: + return UIImage(systemName: "music.mic") + case .bar: + return UIImage(systemName: "wineglass") + case .ramen: + return UIImage(systemName: "leaf") + case .cafe: + return UIImage(systemName: "cup.and.saucer") + case .okonomiyaki: + return UIImage(systemName: "flame") + case .korean: + return UIImage(systemName: "circle.grid.cross") + case .other: + return UIImage(systemName: "questionmark.circle") } } From 5c7a76e188a167aae741ce50f96d914052712fd3 Mon Sep 17 00:00:00 2001 From: coby5502 Date: Wed, 23 Apr 2025 00:54:10 +0900 Subject: [PATCH 31/32] [FIX] set search --- .../Presentation/Search/SearchStore.swift | 18 ++++++++---------- .../Presentation/Search/SearchView.swift | 7 +++++-- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/HotSpot/Sources/Presentation/Search/SearchStore.swift b/HotSpot/Sources/Presentation/Search/SearchStore.swift index 06b0cdd..630752f 100644 --- a/HotSpot/Sources/Presentation/Search/SearchStore.swift +++ b/HotSpot/Sources/Presentation/Search/SearchStore.swift @@ -13,11 +13,11 @@ struct SearchStore { var error: ShopError? = nil var currentPage: Int = 1 var isLastPage: Bool = false - var isInitialSearch: Bool = true } enum Action { - case search(String) + case search + case updateSearchText(String) case updateShops([ShopModel]) case handleError(Error) case clearError @@ -47,24 +47,22 @@ struct SearchStore { state.error = nil return .none - case let .search(text): - if !state.isInitialSearch && text == state.searchText { - return .none - } - + case let .updateSearchText(text): state.searchText = text + return .none + + case .search: state.currentPage = 1 state.isLastPage = false - state.isInitialSearch = false - return .run { send in + return .run { [state] send in do { let request = ShopSearchRequestDTO( lat: userDefaults.location.latitude, lng: userDefaults.location.longitude, range: userDefaults.range, count: nil, - name: text.isEmpty ? nil : text, + name: state.searchText.isEmpty ? nil : state.searchText, genres: !userDefaults.genres.isEmpty ? userDefaults.genres : nil, start: nil, budgets: !userDefaults.budgets.isEmpty ? userDefaults.budgets : nil, diff --git a/HotSpot/Sources/Presentation/Search/SearchView.swift b/HotSpot/Sources/Presentation/Search/SearchView.swift index 99efa48..e798140 100644 --- a/HotSpot/Sources/Presentation/Search/SearchView.swift +++ b/HotSpot/Sources/Presentation/Search/SearchView.swift @@ -26,7 +26,10 @@ struct SearchView: View { SearchBar( searchText: viewStore.searchText, - onSearch: { viewStore.send(.search($0)) }, + onSearch: { text in + viewStore.send(.updateSearchText(text)) + viewStore.send(.search) + }, isSearchFocused: $isSearchFocused ) .padding(.horizontal, BaseSize.horizantalPadding) @@ -46,7 +49,7 @@ struct SearchView: View { } .background(Color.backgroundNormalNormal) .onAppear { - viewStore.send(.search("")) + viewStore.send(.search) } .onChange(of: viewStore.error) { error in if let error = error { From eb1368d1948f1ce06ebf8b81eee1f6ac2fc6f9d4 Mon Sep 17 00:00:00 2001 From: coby5502 Date: Wed, 23 Apr 2025 01:05:52 +0900 Subject: [PATCH 32/32] [ADD] add logo --- .../AppIcon.appiconset/120 1.png | Bin 0 -> 8881 bytes .../AppIcon.appiconset/120.png | Bin 0 -> 8881 bytes .../AppIcon.appiconset/152.png | Bin 0 -> 11588 bytes .../AppIcon.appiconset/167.png | Bin 0 -> 12684 bytes .../AppIcon.appiconset/180.png | Bin 0 -> 13789 bytes .../Assets.xcassets/AppIcon.appiconset/20.png | Bin 0 -> 984 bytes .../Assets.xcassets/AppIcon.appiconset/29.png | Bin 0 -> 1629 bytes .../AppIcon.appiconset/40 1.png | Bin 0 -> 2452 bytes .../AppIcon.appiconset/40 2.png | Bin 0 -> 2452 bytes .../Assets.xcassets/AppIcon.appiconset/40.png | Bin 0 -> 2452 bytes .../AppIcon.appiconset/58 1.png | Bin 0 -> 3811 bytes .../Assets.xcassets/AppIcon.appiconset/58.png | Bin 0 -> 3811 bytes .../Assets.xcassets/AppIcon.appiconset/60.png | Bin 0 -> 4051 bytes .../Assets.xcassets/AppIcon.appiconset/76.png | Bin 0 -> 5319 bytes .../AppIcon.appiconset/80 1.png | Bin 0 -> 5734 bytes .../Assets.xcassets/AppIcon.appiconset/80.png | Bin 0 -> 5734 bytes .../Assets.xcassets/AppIcon.appiconset/87.png | Bin 0 -> 6222 bytes .../AppIcon.appiconset/Contents.json | 18 ++++++++++++++++++ .../AppIcon.appiconset/appstore.png | Bin 0 -> 110805 bytes 19 files changed, 18 insertions(+) create mode 100644 HotSpot/Resources/Assets.xcassets/AppIcon.appiconset/120 1.png create mode 100644 HotSpot/Resources/Assets.xcassets/AppIcon.appiconset/120.png create mode 100644 HotSpot/Resources/Assets.xcassets/AppIcon.appiconset/152.png create mode 100644 HotSpot/Resources/Assets.xcassets/AppIcon.appiconset/167.png create mode 100644 HotSpot/Resources/Assets.xcassets/AppIcon.appiconset/180.png create mode 100644 HotSpot/Resources/Assets.xcassets/AppIcon.appiconset/20.png create mode 100644 HotSpot/Resources/Assets.xcassets/AppIcon.appiconset/29.png create mode 100644 HotSpot/Resources/Assets.xcassets/AppIcon.appiconset/40 1.png create mode 100644 HotSpot/Resources/Assets.xcassets/AppIcon.appiconset/40 2.png create mode 100644 HotSpot/Resources/Assets.xcassets/AppIcon.appiconset/40.png create mode 100644 HotSpot/Resources/Assets.xcassets/AppIcon.appiconset/58 1.png create mode 100644 HotSpot/Resources/Assets.xcassets/AppIcon.appiconset/58.png create mode 100644 HotSpot/Resources/Assets.xcassets/AppIcon.appiconset/60.png create mode 100644 HotSpot/Resources/Assets.xcassets/AppIcon.appiconset/76.png create mode 100644 HotSpot/Resources/Assets.xcassets/AppIcon.appiconset/80 1.png create mode 100644 HotSpot/Resources/Assets.xcassets/AppIcon.appiconset/80.png create mode 100644 HotSpot/Resources/Assets.xcassets/AppIcon.appiconset/87.png create mode 100644 HotSpot/Resources/Assets.xcassets/AppIcon.appiconset/appstore.png diff --git a/HotSpot/Resources/Assets.xcassets/AppIcon.appiconset/120 1.png b/HotSpot/Resources/Assets.xcassets/AppIcon.appiconset/120 1.png new file mode 100644 index 0000000000000000000000000000000000000000..aa3dbf536f474e9421c12199a2b4a6f981bf80a3 GIT binary patch literal 8881 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 literal 0 HcmV?d00001 diff --git a/HotSpot/Resources/Assets.xcassets/AppIcon.appiconset/120.png b/HotSpot/Resources/Assets.xcassets/AppIcon.appiconset/120.png new file mode 100644 index 0000000000000000000000000000000000000000..aa3dbf536f474e9421c12199a2b4a6f981bf80a3 GIT binary patch literal 8881 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 literal 0 HcmV?d00001 diff --git a/HotSpot/Resources/Assets.xcassets/AppIcon.appiconset/152.png b/HotSpot/Resources/Assets.xcassets/AppIcon.appiconset/152.png new file mode 100644 index 0000000000000000000000000000000000000000..06b75dc3543be1e8bac80cc9689c6f8901117aae GIT binary patch literal 11588 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 literal 0 HcmV?d00001 diff --git a/HotSpot/Resources/Assets.xcassets/AppIcon.appiconset/167.png b/HotSpot/Resources/Assets.xcassets/AppIcon.appiconset/167.png new file mode 100644 index 0000000000000000000000000000000000000000..53fdbc611dcfc17b9870298bcf627b308ca5c330 GIT binary patch literal 12684 zcmb`OMOa)-(}r<^I|O$pxD4(NgS)%K;7*Vr!7aGE26uNzaEHO&-GY9+>wo(%&gosB zb9GgBbv30+gSPlbL6dN4_T%qkD zY2d!lm1O?>`r$b#m@dnnq;RoDh1L4#Z92c^GwIv?9I!c~ z-za+->QhFE9z>a^(uW_^P|%G@2#J%$fKK57qYjcfn=fN2304~t8H^1?oQ%jOxF1pn zReX(D#!&9VuTv}RPD2FZKx6PjF#sq%Z0GUR`pQkoOr=F&tp+okW#yongaqPJ`I-KB~7rhS07rtB>joS)M+w-!A4U(YBK` zKyX=pP`PjfC?AToH$Gigy(n=@C}MG{5=eXiQ}%9*|2;p@AB@=%RH=%bam9LQ7?1K3I>JmPi*FO){) zXaYE?@NkH+ps&>&6hq6T$0hoAJZREkyrktNkTTqN;#JJ}mIlao1Zt!GliPLqGX^0( zo;bEyzO_r5KLj7D&#YRiAr>artz9}+-jE4&zrbjAH7Z+s6J`A34A0t|7elgc1o>vy z`mTX|gXpqR-^K^x3Eh#y28p7ARHXCboY56l%RC@uPSW~@!MFlq?nsPW%P|>iP0NRL z!?D9gwL|Twk}-s}Q-(j_S_r9h6m`G35kUKf9ipp!gu?*Z-pWFkPJzrrB)v`M*AO_LJ;!mx28FpbY-Le7SNVOp+IS2UcJ1-mNMx-q8kB zeqd*w2#GoG#+A8p$%$~W+9+L8tyRVkd9nyx=fTm!7o#%A5p&1kNqzhLb?jkk;80p? zL%hwT6Pv}}89J!-d@MWvw@_NcP$z@qIwDw2z%#B3U3j58>eANGOr*)TlhuxE+AapZ z%il*iSWQMQd<3-vUWS`$>*&WV$Zr_ayPke=RKw$vpKfsITqFOv`huXU+6R?XODNVraY!MNpt0n@>D+&F z`6k9!5FkrFm(0~lEWy!GJ%jCDA=fVF8X#de0$Ry{G;$HiYoRu&0k8xg)7`u~_< zQV;l5i0zcZGEn;KBlz#BRB~*Q^`*vr7iJdKH=9wyb>N-^%D{d)BP-2?0z$I6(lgOd z5KT{uK3F&L-R2FE<)V%F`@B=2WT_c>{RW++FEPL-&w+%nae8~aF_VqUAA@NXKtt}u z{IaF8nQcJyPF74c0N#JxzW0|meq6>V<`_74cYjC`^a)^rDuv^VL3L*)e9na5KB=N* zWp`9{&5_Ve#s3>pBHfJqQD`z-r94P=Z6gf*$j69 zN~iQSbUmQn-&OB!E54h!AAhU$B{W&g*h=ZQ%WgGj$sw58qI#*HaS$&cN8xy>8BTlR zpcK(kaUdEc9@gWQQpR@qQ+VpS!iTZswkwy2DC4Y_*wqtl<3ZT%?wmE$GwfMhTcE=%=kwXd_S%+cD1(Y`a-}ggSe+k5ew44 z3$YWns2~``Q@$w;J03b}(F-l>E0QLi9 zKWIybX!B*)%Lc|+*@Z*b*aU%NJ;z*z+=X&8MKmO7F?jy>u1TgRFf= z<4e*?C9y2O_jhT}{nxfaiDo}6k`*?TQ`?P5nLsIQK&-9)oK%c9J`8*o1AQAznLnAN2EWQhu9GaOGFR7hSKri$*e% zRcDMv%^nPAh?uV+L}L=-?+gd1euQw<>7o34t6t}_+LL$J7<5^N`~fu|M4Jn*aY6O{ zb(t86-eWNIw$PLWQDQtQ+%s(3OEtQ4ab*gef~iYmf5c$0a_vtFcc%K5f7~WG;IF(s z8{jkVhPo4I;#_S#Ak}sstVcjuO9UnbpK7KOhYv*$1ox2+6hxh38aJmTt9RVG-y=P> zKTDojKZihuviWjK4PW1>OefcDV*pi731}*e2ryhRfeQZ!VhpG(t149!Fd(qhIx#MY-BjuEH%dfo`z#VlpZatp1%>+^AW!o?;DCpa|x`jd4w zMABUl4hV%59)gPm=O3|MN6WUmmU~Tn5qY1QVx%D)xAd?PqpF9%q^rn5-(uD=^N70^ zeCO0m)^;>uAo$cq@ia-ue&y;V=+j_CI$P>&Q(;0zowR%Pl;kY?thy%^w@;?AOoO-E zGxW@b>y$a3)64kp7muA7ejo`nZMkbbN6l0Kc4^64o;A_jX7idI%OFz6;ZA0TXORmW zN0LB3GGOXdnVwDSaSzbixp7eP#m~n1ay?g%g)caQzPRW103oYe%F6YYoYQ845{iJ+ z=Zw`RCN0r}k@=`2zTBr6ukp+Lcin$5n&+J)D$hA)i;rIbyd9&lvh;Hxvxg`|=%jF& zBW=AmQUN1wq=iX2iBkpDY_pL-^gd5RFfeZ~$2Us|qkPyNw?=^rRir*4gQNcEX1o?hvnA5+lQrnNzUkiCqG~;t)-24w3#TZ*n6}70$*-F$G>l1 zs*1S-pH%CekOrk3WELL6*lE=sKCx@s6~A*oGy0Pt#DtC3Smw^lTnF{@ND{}jN`~mz ztiu9latGaj^#~U8tB%n)y}j1v1}R|X{fj7Rf2hI#bTyul+>IRnm6qjFUF1j4MbOi&VCtm zg7ZNLSOF@TIWA}uzBoGh_yqeN$TW;vq@0BiYi@1I1Z+NC-yHgZ%d1ps8h%~WsXd$A3Y5sfkZbY$5YJn@fY7h z@(8=v7t=2hljT3!F2p{c_h_J7BY5B79PbOH1ETmL9%KVKh;yPaZr^)m3>R>KpPSpTopRYLA{Y7xq}N zGEcOknPE-Z!C4f|H@p6v5Z6w={ob=9s#&zS=Xh2tqX;r~EtEES&x=biKKg5a0m`O= zoG$e=h(UY!Kw?|RHau|w-CRY6I}>Uhd<0XO32!hlA|F%1RkmKkuXUYsa<gTj2S`>SIppZ8@$V3R;(?%2B67~cPqq7E*76;-G|7PMUXxB0wX5PZ(;w@}dSE)rg1=&oT z=6UR-yP8?MYsY_T5j-$8-}A?8K;U?X$&sueCyF@%^%;fatUlC3f{)}gPk6%tVNZS3 zmZh24?1}S5nmFPXlvmPN*;bLd>kqFFGwr5RDO~9bIugQ39#4Eg=)3}(<<|S_rgq2& z$X*drVaG_Q_01(E7TMBlw!j($W}}NF3PFosj^UEoR*<4wiu^Lf;!bKJIra}Z@2)F| z>gxpLy+Y&+?RGc0aBZ?305!3e&4GmR05`f{=mppeXqWTxzz6&^kdU;U<4E1=?7kh) zZSoW0DsxLVy;!6)v4Hd5LcqA5e?RbJ8h^u<4GS<4K?h#0o5pGqW>6GyrBn3hWy*Hf>f^v34ULbS(nj>Y~p$v>zY7ez!? zrdVBze*@5z>XR;}eGzr;)d*VA3oHDsqfDn%d5Q^2oG;37rQXSa(kDdi4BH=4!yq1< z#CTQycBc*SLb)-ERm%b{Vi>o-TqgSz`#lrBT=d;`;K4OPw$)xy=#s|0guO;%o25|D zES}5~UU>}dRy}0zOv;fOKmj9(`MM05#$E`-3r1wqBPXpwbbTVEjcfe*&8*<-oneIU zN+6XQVu#l^14WO^5%vSR17y_mnwy{R^B`R?4Eer&ZmQfjeeKyKB;;?AggiU3{mv#5 zRp1Y)p+4CP@PYGZn8DD_Xd$mMOc%55ZgZl~S??mFyB1m2NK*UVJAQVEieY^769>|T z!6v8Wo-P^e+4q%dP#iOPbe4+qCT%)CR}44UOl9VJS-+=S-sK`PL`|l)@_9YH;br_+ zweCt?#Z-#rIqD_cK8nhEdzRFXJn%*%xJ``i`GmzD=Z}K1d1fv z@NjM{7~X%GC@b|=kix&y@?-bYJ;Rt~Z5WV&IZb&UOpk{j{Y02;`ABSA5&>Q|Q-Y$W z%(ce7VyYHgzFl7c8z%} z_p7P_Nvi+AF+M`z53f{YaFQmCg}KxaaIj#l#@s5T)e3FCje)Jyn94Q_NA;*|KOBkP z-*5I`E7zg>3N$f21k$1458}xYdrnlq-<7bM>T_2bK?9FyB>{L>xNc%xS6PU=jUA5b zw;;3-zDk!Kc@xmh@`1)%%5IiW%UJWIz*jBE% z@etf^raWJZVEK(j_)1tJ{)5Hea>wb7guke`k%z~N!2*XYRCZInn$F(mCjwIjCt?;7b;kd8liV!EMz3qy8d+9Td=U01$6EJ`63+)8N?cV*fS4 z#m{ES!OQ(JGLIpf0*a&lVkEh7rG5gw4SG@UH`Z=XUQ5kZIf? zM5QjtB9{7*v24S}vClf08>cqbUv4=&GEi-BG8PCx@kD->!DVeLozi;f@87)&zbDjg z9M&}C^Zem9Z6^6ngDaFsxOe%DEa@djY&}4=4Kc2vR#gua88ISxd<&8t?y5frvcaR- z3zME!Y^p0XF~R^zHU(-fC(?5M)mFvX|3-01VPnuN>;Rqs4%H-uehJ||?gcPv&{r@B zq6ZFnse(a?*7GJsTowr@tL41Q+s?Em+gno>NI3b|?=7C>hW50XODmxz=m77L*7TK1 zR^EL+oh|GK8u!jtU&8%xa0Oi5Z0SRGj6BC>YIztRzeLxIAHfoA=&qOe56z4SEYU0( z&by&MuM-gqw11(>(mLF^Shj-_;gZLUwD~JO@3$K{s{Y7){12H6w$L3stVI0rTDLMF(n$=c!HH(4*q|lF_ z#&ZO%VH;K(Fxa}t_m{0Q@Wj7@sw6QtiQYXeC)owxar8Y-LG5RF(vf$IlOU!fiZJ{g zzL?iOZs`vc;LTGRw~zKX;4@$~H%wU_Mu}Eg<~++R(#MV>yfZ3QT{X=89@uQq93zs@ zsac9**~!b-rMy}8ZWC;6SB+dj~0Z} z5%n2sM+0?!qe`l+vT!ESs{*29+NBg*R3Sd59NR6ozF!cmtnCI`$TE|7?$H#OaAECT zej?UewWS);4RdQlX4URO=A|-LlrQBgp=`^|WEwYAlzdDj31}0$%0Z|GoYIuir zJ`gS}xmbftv(_rJ_^)t?GQIMd2D(yr7^{|W&0h9?S6*$uFF2_7Q;g)8j`l-~@6R0$ zy5=x>#97VJGI7OlrqTWqE<$p!*J}@8zXFw;dn#k;Aplptf3v*RDUWB1=p< z400m9qlt|b)>?G+;ThlS>iVv6(1D`_r+z9Bn%+0>6U|JoFy_s%!SrFZ7pO*`nWSsY z!oXK-km)+vo60J5jfs;*ZmJP@TbG@>JNE}~6~OTBCe5%?dt_=6oWx^oT@y?`sO2c6 z2$`9bBm`8*h}3g2{FR#dOO7LwfZI60WNWP|!gX>y0J*eeTwPq2e6sP&oC;Q>lih}c z_#ZORs$io{I?L_i3Ke3PTtiFusa(~{M;67ew8+4u&LcCdso}Mt%O5z0YYvRP4ti** zM+V>0!BAPL7EJxH$DZXIJ`r2k^c%3DBRfi`o(^Z;-w7z1&=P$k)NP~%Gx=P9VTQ;@Aj_Kh<2?j7K>w3wpHl-!p{}`9tWN$HCzH;r2khg;8 zu^*#HK2bOOUJl0x#0BZ@S=#kjH~gxf0+IH1%C))6WfSLZ_yzn8?o(TpQGDh$VgC30 z;2ALWlA_T-l@G@B+>0)R+M2Hw44^xA|3Kf-K}{|u;6wDjdZQ_SYerSyPIthkuk2Tw zB)ea62{#sI`@oVUpgv}LKdP`%#sOOPwI$0Z)FV6XFgG0 zl;P)sIq9Gz;CVIcZ2*vax1V6U*V_Kj{q8j)`BZ_>l&xeGLPeITnWVf`@`X8R|$#=eE4BNjRWXv;o1Rjg5H;gzI5{87|n z>@lfVuO9LpV5)PC|CIkgDKUrITm8_mNArX5GjkYZZKe-ejii}Y#SHZJp7QJL^wi=1 zW@G<(nb`$!-TSQkM8VkgGangxDmcNBJaUPY1sbK8e7%_3gvlO%CSl9F)TkQlkvnVz zUsJ9<_;(p`H$TFz5fxv zI_l6{b>bvs`DNnpAQN!deP7nH>~N@qYTckL56 z_Sn_6y)&+9xYaiXiTe^2ZeG5qyKe@?=t4GKUS286#m63gmNmE7@&uup>{m_b>tJ%Hm$e0l|=QJ{e z|JVE;~cb?!wjv`KF2SJVt#sIR=!U^OVDrQT&0O ziXKi!(eu42!}oQ$+0>B(p~#|+P)lf@d?Hk`&5o7bBk`>7{FPAZ z0Y@yctJas>5d0|8Yp_1}o~O>{>$%~>osvzz|3C_9bjx+Zb5%`YBk%lus8^P!2=D$9 zg0@tewg;`nEG0E76orcv=B$g-<-BkgXx!#;68d|FXmwUiWlGis0fs8u%3hH>LaKPqh-)Y3Y${$TWYAQ^}%AAm|Nl4=K z$Q#;dM0=4>_dV7;;TxK`FaPyhBk7wjjuPzJOlqn{Yl8LBKr6aSrsqEeR^sj|A6&G{ zR=GNK)l?Z^0zE-00JbGmvvtgg&OVeZ9uRr9yR^z zq<2=nqQ7}Ab)e4tl^`xQ@P{U4=WU81pZ9)aNNo*m=_~B{F6bS{u5vTN9(3cCN{DNK ztoMt4_`M$bX2RjV!e|g0%!0J4sf6s&YG`FHUh~#M=^Dw2RK6l9iFFCcL5-^8#pn`MOfLp|MaRFW+ibQKoxn$WIF!Za4MiShiaI054WjWuOMK zxHRJLH41XKnBiSrc$N=dLPE=*)SZ=jSjYw5iI}4d7n5gwg}<#VRnC}G`i$EOI_8IzQi%qrHoWQW@gbw>c0OJ74+i~S^)f5kKUhx zf+TIeAEXW;7v^@ur)stAA6kR>v;C_b8n_gUCM_aMyGE7T~P-x;tduxL- z>EV4F!rY4>=7z}Y2Wks8#p*&Mz9%-xfE;h5YDZV|MhEIWGk=NIM$GN=##nphF=rSE z4)tWTyNS5xSRKFnrC+pKpwWd6l2Hm2hg=Hd5R|lK4#fTo3rG6fbiW9Ono;gU z`Ds2yjXGV~$TvYd*hyd})4k3X`a?^*GKcS{c?8L503?m{=3iB5Q>rZG%WObD$}zEPS$%+ z3@B9J2UrXmlY{pZGCmfm2es?lPSc5TtT||WxYe8pzB!HsV0@NONU*ufzF*EQSnzCT z-$;XD;bYJwfl|YYy`R_6a8y1^im?UbcZ_8IuYwnkfgPBC(>Fzy+|u{3XB#ZEWz#iY z6eqsdxx8>{yV20QVyEd!a|*RAMO8LvnyCKanh?2&G3<=j2F4L9{hI8h{l!9-;$dMY zIgFd6fPoyoG{i3Qc;G+6;}w7A*|vpoS(`K5V@^;2UY|0!Q1e@Irw4@IDexTDW_UC( z)0`p4M%27{P6@nvPWj7xvHk9Xlsum|vD=l@e+eGro zHh9?|etN)L)w&1EYsXLfGZTY33-`=52+-CTZ4vXZD`%a*_--wP7l z*Wxz@EM)%y#P?Zwq>Pj~Gc%Lq+|xymuaoExTJ5}qS`WSvO)7hE-ADv6%*gyzx3jf{ zjHAtWK3(J2dDAO;`;R;K{(rX~04AuI%sY=%+O{f}&1Sm8Q2J>BaTLASs!7c*=^gHz zf6hCKPPF$0?;M>Ybmf(9)~0|RxZhLPZ;JMl5f1}h+8=Rqe+pM^a+tcX{uI-~5;-T~ zzui*;=ERmCN9nu^tBRbU$@!EDSKEC`18x5)(FJZkaGhICyf6yHC+pl1OD3C0 z|3*y>Tb_oYQkN<3qBz2<#`v>RpKB@r+i-CW^lv5HO55>@P<#MRQV_~5U7idVBU&0$ zY}Mkiq_E%+E(Y`x`L4e~JtgwLzC>d&=1MS0ts?Q9)~njSK;JgUhIV9A$yoI*&HP)3 z0Sa9u7k?$-NO3Qugw9=bYdw~PR9@{k>iaaY_<3oB6=w30d*7T5l_W6wR>FOqMJ zy-a&frA{uE6!-q+eeUk*3Eq|3xo?>}7nxkCg^kwx_!d0u@;M2iy!89GQ|bA8u}VmV)rWKC zcC;jR0+gg84>a^@4l))WqSy$2B+6YH^1puC(?nghSb9B5z9piC9k|pngmvW?;`S9U z5$9D0H&D26a;9$`;B}tFf4fY*j^Y({ES8ov4@h_zR|BaxrUH+vNX~kFGoLI3-4D`- zfE40ymLENy9DsknzjDVxLl;VI=XBmz@f(z>mKCC5sYdonQIjbBG7NWlactg^b!noN zK+%=4IyS_Av8<_0E z**m@B<2)FEyda_VM5LMlUXjiImS{nVRJMiSP_}E@?bWK%=@CwC4`O9sOZb?j#GSFH z;tR&1XHs`gLqSB~_xp?6jbA8{NSBF-T{+)6U_W(q*bq&bnFj|IApli+D-1;*p8zw> z$`PwALhW_LTxmUNy-H!DGpn`Ycr49x+Q8i4!)0T8g{T{MBpqif;fmnV?>pwkO6G4c zefUS#vp*W`L11m^7}uBafq(v&<}f>;+x_a?Z-4xaR`^Lj*2Sysp&GLJd=5b$8|R5? zowY|u-bsTbiF$`5_7JbMXHgYaS0@{i%XXVF*4eQ^Wt6^?oEE;D}}CR*G9lOOI)CtD2bykzGo! zp3wcFLsa6X|&*^j7gvTaEz$i?%u7kx=48l6$bgMg5U1uQ2{DV1cUQd&I6WX z3vEGosPNDfuCc9+_r$YrY4qgA%m1vVUsRa8#&#v!>7lAgEUV(3W>K%_ObMRJZ1yU%Fk@6-kWEz5kBr!i`NGs{`S&W92+o-OyPWw{;Wgo zhC@3QjEN6<^&@Po5B8m1UlQ- zV(Bx552q6q&}>vh5>1(rd{xWx+PtZY7 zHufjCIlprH<83;M`KmG=R=pyJqE1?x8$M7rJxF~Q;(=}^PaeUk^S&dBjn-wXS}&~c zpxI?&7W$-frRdmpN16^xMoRErhalAp*YdEFJk+5x5?X!PS|7pn>aq#1?(1$CbWLHS z4bpAi*fE?Fjd#YT{+-{bdDN zkNal-`vVhulM-ISYsuUB;7|A@o%1-3|TMj4dolz0O&E?O2TKb!PDzv(cY99pV zrC5I6xnicW@f!gw%4Xj91Fdyw1WjDhkkB#Nv}$~=v2@QH+5tF`_VO|F9mqC8XMn9X z_euMRsJdvMAsktetcuCgNiH8nW8VffKsB^P8`tS*xHJ=ps@%G`+(@>vy$B1$EWx6R zoc=^gxh%Oo%YjP<%hyhI$H>mRjCc@%E=*>J4$9$qF^Y0RcO{eLAfK%bD8QkqX-YQ3 zkfWT34S~F{*Y{N~4VPrTNyT(NW%%h3pA^TcWX+<^?_iGU6I`yv-A_G55!&#FBw?jj zTDf5P4zw*;-|O`BOL8(Pt#PEJ1X6Ub^!2=P_~^om{n(XSY<5C&5H4*xc`|F71}l-d zZ$%=iaeEC-2u zhsuq2{u-kxQ9&=J`qU_jPD)>^cdz_cBKJm9m{nys+D2C>c)cI;*sFp*?@V<^)|J`+ zmE~63q<>`*D(B3DM0<1#KXptrSE=0RUYoKWr(aXIVE}UF^ zS@|x!^fT(Ra3?r?KV*h8F($Ci#DkX?DPB1#oFnytRB+`=qza*?nIGPKd2zN7}W|Q zH3F0B4J2Nd?N~JX@JTp_76_!eD4PK|Whl`Ef1ct-s0|nv=n&jVSI({Y2%;7h@m{C! z#^03_p@VWLdu2PJpbkl1xN$z$UB>EtL;vKonFDAJoLA1u5(pWRD&Gi4WpKfUyvifz02Z*%%49hi9*uQsH2@@{@Qp?3B^ zmHOHjN5d&_UEr6HkH+z$Qknuq4aWhRD$_dU486y9tD;*>4GtQ8<%8yM9pMo43)|>C zNq_vphZM}$V@C#agUrP>D`lxH(Par|hKu<}zh;e|*|ksCmuttkqS>{7(r6aL98K*G ztQh#ZcDBD!-F@`!F;t6qX5vSmA&GE(F~df(BJBT`9SnE9XB0N6<7`o3A~`5(ud4Sl zK+h|yYEf{C2=Zwt&=4lYlL|tb?MxbDr91v{xW#=p5Q;Eo;)D8ojC}UD_a*JVV6Ejo zgpmlWVr&o><9->UORCAsfbTqCwZMjGa$17qhjH@n_2Sx%}2PtV!K*({t_ycB2=J?zj^TDK?1Vt7H4uVh}8Sm=URxR80 z3n4#lB|b8XLI-wDfY=M^BgbabBY-dI2l<~Ur?l`bD~&TI%5lUZbzd2m=W|mza#5dq z;KG4AKu-G{)7=rga7V~M#IJl>Wm!yk=U1pXceeFo7PhD>6@f+r;qDWgOy?xVA1SuY zU<+T5r22%EYAW{5?wCCLU?-}fWOuxDexX&~lGi5QQS_!Y>Pj4AqF2gGMdy6Ip#S`4 z?fMQ~iJXmwYLvpWmjlR~IPY9JB3P=&`!LRyv(0YB(bdf<(bCN+Buva?UIoawl1toD zoWOzH4J%viv<7t!eST!a4AibIvj*;JpVXVpm&&wZ{5MXVx6muP)4 z1<%Ff5YK<;_oC+*e_)E!9L34@h(*eFzqBzY;xXS#BaS1U|Eam-Vl6Cfr8*aFW{`wP zW}Zm7rS4*HVLlZ9)k>pXincv>i|X?pqbG5QHp8GVQhNwU>>KVgs<`nf$q`luJ-iZ? zIiZ_>b`(@h|3CE(sI#HheRBRBrlnH;Y@ZDaaQ~-aq4xuFP6DEk=h~I+v%LaBRuU-j ITg*84e=s|`Z2$lO literal 0 HcmV?d00001 diff --git a/HotSpot/Resources/Assets.xcassets/AppIcon.appiconset/180.png b/HotSpot/Resources/Assets.xcassets/AppIcon.appiconset/180.png new file mode 100644 index 0000000000000000000000000000000000000000..e1a4894975e6aed130d3fb8933b8783dadd8757b GIT binary patch literal 13789 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 literal 0 HcmV?d00001 diff --git a/HotSpot/Resources/Assets.xcassets/AppIcon.appiconset/20.png b/HotSpot/Resources/Assets.xcassets/AppIcon.appiconset/20.png new file mode 100644 index 0000000000000000000000000000000000000000..ec18a8af34d4b6cab533f8f399187c5c52a911fd GIT binary patch literal 984 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 b{~-J~Nzm>7q;sXo00000NkvXXu0mjfJqQD7 literal 0 HcmV?d00001 diff --git a/HotSpot/Resources/Assets.xcassets/AppIcon.appiconset/40 1.png b/HotSpot/Resources/Assets.xcassets/AppIcon.appiconset/40 1.png new file mode 100644 index 0000000000000000000000000000000000000000..7856fd1e0fe1418bd653615ab38651ef0ebd6929 GIT binary patch literal 2452 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 S+;%Px;PDw;TR9HvdSbK0(f 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 S+;%Px;PDw;TR9HvdSbK0(f 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 Z{|7*&`Y7YALYn{p002ovPDHLkV1mN@Cuslx literal 0 HcmV?d00001 diff --git a/HotSpot/Resources/Assets.xcassets/AppIcon.appiconset/58.png b/HotSpot/Resources/Assets.xcassets/AppIcon.appiconset/58.png new file mode 100644 index 0000000000000000000000000000000000000000..d920adccdc70b7ebc448c9195fabffff04adf265 GIT binary patch literal 3811 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 Z{|7*&`Y7YALYn{p002ovPDHLkV1mN@Cuslx literal 0 HcmV?d00001 diff --git a/HotSpot/Resources/Assets.xcassets/AppIcon.appiconset/60.png b/HotSpot/Resources/Assets.xcassets/AppIcon.appiconset/60.png new file mode 100644 index 0000000000000000000000000000000000000000..ad66aa5c17701e80b09c17d1e73247ff15ddbdbb GIT binary patch literal 4051 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 Z{|CYuYid6g+}8jA002ovPDHLkV1ltd3J(AP literal 0 HcmV?d00001 diff --git a/HotSpot/Resources/Assets.xcassets/AppIcon.appiconset/80 1.png b/HotSpot/Resources/Assets.xcassets/AppIcon.appiconset/80 1.png new file mode 100644 index 0000000000000000000000000000000000000000..f889408abb632b2545c0480f1ea546e1096b51ed GIT binary patch literal 5734 zcmV-s7MbaZP)Py0AW1|)RCr$PTnBVjRl5FeFX<4{Ak=_~fFmH5g%uR+%2S_(u{^9iWtM=zNJr3N zM6f&fu%e>l%;>1tL9vYtPXwhTgg_`M5JQJesfhexnXB z>kTmIOboZ?*MeY=8H2cP>@omJG4(nFgXYpm%7A2}4ZWj_3Jq3t%O~#gGq0O>R031q(~1R695GedN>phQcNkhJbf}Q?QuU+ zO<4hNsOQnX8sV>uduoWGrZ`M5bOlPsdVUH5mKo(1G%y8eY{jQ8C9NgBpR)} zp3b^D4L|~~L2=`>q;v7RzN?U7?#D))Y+5umm1-z$n~>6$ph@XEYAf5`(;5`enHq6G7}00BL~@iU;G;Z^M1PSHh?x!>tG!66TVi(OT z2*Mi-TBA~pJ+4jI-SiX6T-#CYI{=4T3q|wr_Yo&dGNz-4xi509Ly%`5iN5yJVbv!# ze?Nf{Y9r(gw5oug$6FP@$2*mO>=Y2=0+1xEUUB1wtf{!I=dysxhF7iNk5v2~Y^whR zKh(aB9gc5N<2wYOU&xU`N1+?QjD|xz@aL<35#CvJK!&9+h9-?+J4^qTQcHJ)GZ|9q z3Fx;fX5yX7$KzJ#4g)B`yDa?<{CA&M10FVeg&T%eio%o+XWzcz@)dp z08)8fK!!<=1yogHF|h(~{1|{Jgas;0tHb)(#2BAYj*m_3@weF!$h?cz}HbaxAAH&6Ev`K7l39r8T0)TMmje$WHEcZAOi+p24)~ z&Cuz@CO8!jtOhG4@XYN3 zkYZb+D;Iaa8NchhIxJO@l2_-e#G>7oVVCn4&b-ivD4Gv$O~V-}V{v`fTofdp+YB20 zJ$Y!U^1*0Id)S(gZ>nE|zZ_hI9gdAiFc_N&d!5gXvs1@ndjI#?i-&VAlHxqK?^b+q ze5;0IMp@lM5jb}(kWg(a@e=fXK@m*V&RKNL=mVZamoUMU3fwPe_U*rOm0`==C+PVp-`# zq!@`h_|d3%F}2^zxUk#AW1zO-WtO5HQs=iTW?^;3ACO|Qa8`y0_o6{xV{qbyp;bAM z_OUBU$0p)}#l)Nz1 zQ2-QiDedO6{B6i$`_Yn=O53-&;S($@xdI6WGw-(7ui~*mKVVS8*%4#Y&ai!ycn1; z3`$Vs?+PDEnlZiTG#qg4 zgw0?_ozjF`vS;DuoJT{N6&btFDn1)Uj*YPCEgXu5yCMQ8)%VJbdvH(gi)~&gGssP9 zJ*E`qqtmDzQN3|g04d)$xz7t2n?9)xfKvL1z|JeahX-4<-n*iPb}^~&BLmlANYdyw zwoJb4!>XlNd0+<8j0tE^ofw%s7PIp{4;#dHHJG$cQse!uLRbwp){ut>`~_#HUf;&S zNuelme2+QB=fSKu^LKPWg~`?lhw((gR!%Xsbs!=HdSTxk`0U8*NHr#+R&n5#?1uw3 zrme^BYVaf=Pwl-4UswMbDSJ~*vtFnjVyjwGxEisXS03C3=C zs-O_b#*8-VC;@$>WDGVpe1$}V9ra2RruBOb7j(P5OSxaTjgeaNZsk0@QLzYV#zfR9 z4qVpbzc8uyD{UlDnjD8(gDE=-Kt+0PatagHMFf!AGSbbx@MJ+D7jVMavl&Ihu)Aps zYz7;gstXJ9KErR4#~l~?(i%t!;Fm{M;MskXkZMdoz2d~Ew5u?q|A*{hgA*F^8!1CR zwXgsOy?bHSg;(PU0P5lOdy#GFkL3kBVB*B2=t&WmsL&G8oK`dx2V6z47%cE;9xNXC z9r6>;Js|*;vG;d1Z{ms4Taj$Eqfu#Urde&1t>U`~_-TcMQRXg=Xa}PLm}41$ls{I=!|EGxaO8NiV#;T&BMfJH-4=Gxg&0Cn71)x+GI zRoJ4phgD$;;dVJU!yZuKd3m2;Sn_2jgak?eKRf(9UfBOYvkITrZ9E?8_g(!(Y#nZ=vx#do)?x#e{%R~ z{AvG#tR4>4$@Sue1HU=0QAn_(qolwT@~ED@uV9}d+N6q>KJXNrRYsTS9itU%B_CFygiXO$z4ZJ=x z^-n`#d;VlW5f{ce@Y{&V;n&Nj;I9XtiZ7W!1k^^Q9v!qHq)JLD zd~yFh`1tTjq!{gRs!hmG7>P$u*}!5Gtv|5+vK9Ra89BIB2di(juY%RLPI2Mx+@-iW z_w9nL3Nvwhb0tyUznESN(7OBqFzA_HuXtaVd)RC%lQB(SqETDqS)PApW3r6Ua zzQ|V0<6YZ;1cMDO)s6o4p@Bqf=%RLXIqKLQGJE1sYPncWeJ_+%X;56aBz*!V_jxnq zrplE5cQtQu)FQLkkya4{fT9=XoU|)2v;UfqwFIfgo9aKpBPCZO(P-w7uU_%sXx2j% zdb;A0MJ1Uf673o)cW_ZdR3F=EApsPPpL#6aGald9{=IVmlzfnybe83BMW&@+z!tWw ztIC%@S~3%V00MXeydf3?vr$om{alE!d$=U9U%-?5$xLlb$#fjSo`F?@B#y|_Df zMVR8cXi1^Qr76t+nxfW2*uKXy@-GYPzs;G08?zUMtg=Y}%iM+BF6M_qV}bDc6&~GM zJZL?7SqC3e5G8ojhq$=p3g~oxt_gcJAG(>cur#0gbYxdW=)!t?+dE@5Dz{uOQ7N+S&+2il-;{c?}~{uH^TNR1=jA+cyUy7a!cNOMkYudL>rw ze}JXmq&Ehdi`hpjCp7~J9mNch=Hz5>Cc31l^ISG|Qa2WL-mg8f z3RZ(?V>c=_m|8d(4N5hP$D+MWN+E@IZ+| z@>U{=X5w@@F6=ibUR;wo33und5OI1yYRt+5_u`|f|A|_KYi&qkmO%L+eL$1u#hhl6 z9J<91X`2 z?tT1N_g7SU%3uwQx6*SS&CPUWLe3&wmo+C~$3i!oOZr8PNb`4%Q8L!lD-q>*RqQs)C-D-ncvWx#kEcT<5D?bE9$u zGm1vwu&)f}7#&A2knILgsIq}OPKT$A!CdOtirAvdK9UPJ2R$WNT`?O!)V|A7Vlxzdr+YBad8@?%f0ap+sPcDgeJfG&l z^3v<^W8Ei6YKP;98^B<%EXgU^l1LMo!5>7O3~Q>M=1zM@pj)5LJ|HmJ#?gop4_1qd zfeic^rKqS@>(QwD3P?33@$~zZJs#xw5aHb;k9{3&i24-siZ5sTOzZE+)gcLNwugay z;e?#oxG8%vyMA%9Mrc|h`l4x&Mv&M8?(Nu6`!`T$*8%r7Zb~A_B&>q{u{^reQm=qBKb<3mHHpZwDfNVE|zBX;L!8Y-Pf}8c69JCE&yrOBz2972e$-3ltwsG z3Mn#7$%HH{xjbsl zFVfoLmcZ8Pd{)Xh+~0>HkesN~o52fX^=>{;LRda=00o&tp~S%lQ?wVf;CD#?e`)v} z3ra49U2kSb*O9B!0F;h}a)()#T-=@eJaap`qB)q3EIgiPB!ItiBl=jLw>u#K6d)S7 zy@Q;=#px4qW7a}WBna==6^Bm96Sl-8fpBP{lOB|{nZN52SfjMCb%_cGzeK8+L-iuv z+yhtkn1ZqC_cjmc@NRd?u_uA~cv5j4y*0EgBn^;9v53(yL36e>eua6v#+-NnN-#+p zy_$zlEXuLu;-c<%;DTUYvWqfD-PZaQe+AlDKw;`rcO(`7B|1|bR$J(|jAG0<>!x;YC2?Pqd%;WtTRQLh~0 z-fnXTk_cTUfo-Kr9)A~ze>JL`2s3ZiQ&PbWn<1GSI%S+JLN_OmqPjFE9}PgHgUA1M zIv4{Pp0>x|1x{O_{2te5=ome5cm*AMLR0b8zVf(^%sb{P9MOZx!z3t&$9zu>9Ka|@ z1)IA-7%WcnAU8qhGYIc0c;nH-DWaKzU`IkVVzRx)2xr?w`#oM;EJdYQeyH>Ak&$%k zcv>bGqK}})wifZ8SVmgAoW{iV0slYlIsql{-wW820LHmrCjmSOV4MiV`S?i*j1$2B Y1DSLeB`F)0a{vGU07*qoM6N<$g7awtQUCw| literal 0 HcmV?d00001 diff --git a/HotSpot/Resources/Assets.xcassets/AppIcon.appiconset/80.png b/HotSpot/Resources/Assets.xcassets/AppIcon.appiconset/80.png new file mode 100644 index 0000000000000000000000000000000000000000..f889408abb632b2545c0480f1ea546e1096b51ed GIT binary patch literal 5734 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 Y1DSLeB`F)0a{vGU07*qoM6N<$g7awtQUCw| literal 0 HcmV?d00001 diff --git a/HotSpot/Resources/Assets.xcassets/AppIcon.appiconset/87.png b/HotSpot/Resources/Assets.xcassets/AppIcon.appiconset/87.png new file mode 100644 index 0000000000000000000000000000000000000000..d81ad3d399d13c12fc51ad950d87a2e902406eca GIT binary patch literal 6222 zcmV-U7_sMxP)Py22uVaiRCr$PT?c$rRl5IA%}geh77_@MkdOpy6hXkQD7sis6ckZcpJmb2g%wbY zC1r!f{1R+DN5x?s+ z^rM9qdT2FThMGWE#T_U7I-%eapahDdy5Ufra4JFu4I03rk3)j41LE}UVbLYQs5L_i z9o%riuGFL6U5y&oVbr)PQ17mXQx$+4w181#fL5d9mvkb5J|W=J#-XaPD-JkRz^Db% zVzQ7GI}q8Hv(Vjq8af#>kf86#pa#3_N*#{44&tD_1ZCE(*jc{;#dRA|?$`~x0>o&5 z7_E^-onByTU#$u*fvTzs>J=wTSvOM-1}0sN{)ty0)7&p8kR*LRZk24T4TzOPv0``q zkNENEr`TBi5z1@@^d1qbGcfJ6CdjPJL86Tl zV-kECR$}~bK?P2zGs&~!K0Xzm%cXnKA{v#ZjIutt;6}Y0)1dK_W4wE*XH8*XTw0>(}5`|8uz2gRAV#X45GG;Qb5vY;yDM=C8rbHL?Po$Ty z03^*sa-YhQO6#q3?#0t3H(+bcS|qeEz%2$YE%b=Xh9OBKFd<_JOxidG)?cXv221q< z>i+`Bq^fZ4MTO%xwq>o#QI>wa#(-E|5|Rv^(cX}bc7`qup25doRU+yIRoR*bP0*eQ|31Mf}sz*u8;i7mkYPWzZ=h8d_L77Aq^3v>3q623Jx6 zN$ljzrF^9Mh=w@Ubo?b(l5$yJ{Vu+)ScJm5b*ObaSly8utJCOM`N^1rU|SCxd*Tl+ zMPb*Mz)#XA;FS1_a8di)&_8j6*D?_d9h@%wUF3PYM`2CXhb=-8%>tJ;U+1?RawG8AlVWz0%5)hvr6!|KVT|c zssmQH!rzq_HyGEXJcI!eDpUcmhAoNd#TQ{)?RSXNHP4DTZg6R1PSu4(eJ9M#-H3LE zu2A3(VP*+KjE*3=r;GMZz}A{I99M|Z#z_6o{OeR5aCm|^bo`B49gG?iG#rd^f%w>= z5}06kW3(jcF?<>XeJHTH9sKV@lCHrmUFUJEA{gFnX! zt}ub@iVKfre~we*hw-L~YXe;}>Pq)4JNOLV*#8jhiVYT>i6xT)E2R9aZqndDYD^FG zvJ6JHWe~cW@{nvugIOET-|teL91$Z$M;yJg?t2c{9JH77qs=-BKl<$GT9+N|3>|Ps z`tul)JemOx<-Ev!{95xB9@}{dj9LO$xYHi2R!P0Fefq1ZR07|XfVbU6z z_L_1v9bAnT^?&f$qgl&wM#Au*Jmw3aD44r@Bmu#4RxQD6dne+squlF#(Q~VuHe8c39=E5z5R%FyKT}lgv7MJ9mceaWhNDT~ z(h;k3*>P6V<#@2?vXBZwa@Rlh+=fpNEQlt&W118XZW6e( zeTS;x$zJPN*#y(R^3RtaT8J0Q?q@1sH`oL0wJ8&DOVq$h z9`djz7Y>95v>C8sA@_Jx=Lxu_%Yqh_)?k~JrXeZSdAqO0nyRHpVChrYj7&D9;Hlg# zNYHoin1TWJ1pN|61UXI=ClE^PvEuVM7-AtC zRs+&Rw}B$yMalmz>nzjH8c=ea1X+r<1yyiPCsvW~Z6e78j*q*nx< zwjBKe(~B=_68A;~E*&F>;y_nZHs5U&?rAt~sANta@3&(DR)7J%yHy>I{i2>xgP%!mU!5rHcdnG3UetwoMy zNI-z3)P7g>4rcEj&6<_&qxkM69d5$C-QS2X1Q{)OeWA@qiiUFRAXX=AV5jQF?40k> zGxp3NdlP~FUGIfF(ZRc#QH17Rjke=Ag;$hfyWF;(m zau%EbjF_Ld1*tLF?2!dhg3*FA7(Hw~`UM^<9`5B{5`Yn%?!ukvF9oFrA~HIa4-}k= zz1AWaBcO(`;F3RI>$2g(_BY`E9&ZJFxCE9sZDPSG%unRG)wpc9p#AmuTaUL-7#$o7 zu;j{z3kRW~ZWCvLDc{%Lkcy}C3Sibzu1}=t8|o7wy)TsAiZ2c?jwmD<7F@}<R zgL|Fwz7>aG!gFPRX&G>d7O3oKMz62YC+_@^oV1|iI;^gI5Ak|4?1~i~jp`tnM0)k%R&`EVxe9iB87ucsg%8jM~^n5-1;f&{2ZF@8|=k zYKLAE!;)}U`h1M&H1Pym360N(JmRnXb~ipb_%agpX4n*~XP-N`X00*r`h;lWjy~9L zFNRSQ+TINbE`{=}iUYkY{V_XdQ@~R!k4r)A8srz9%UYBqzCHrq{Y210L*8A87x&!H z;8F~NvK{mDf}K#hQvjS_Gz8mgzk^vD6EX}L8eEMI^=>=*C0v9@vQ`E>F4Df;Q2joh z+%?MUdsFOTPVSHBVLpRHbxo;5h&Jd_AbzO+5Krv7#H9Pokih<#JB2n$MYV=YQCTT&A9bALrjOlH8B5^VrO zeZK^kQuxz~E)+Gq04wg9y}m_W+&Li?_xzGk__p%>up!8Zz&)qkNIa17LBI$4+{#z* zZ0VgIa2+se%Q6Wxiqxo zR=$P>C4cslT!Tk)d$w6{Dev~^&Wo8lNr6#tYE}-p_(LNyIl~3d!%8j%HU=aOLw?UM zgK)0!Tz)vR6i*b7Y`|Zn1T&ex+8XDIO!arw?_&0@>p2ZXx`QmAIXRoTZ6MH^eVqFz zY^eUMnVjpf7INbT;)z}xyxKC5TS$q)sYOHBJfV~g6^rEeT#5mSSG5_}_t*&8#|NLn zE5A)|Dv6FO=PaFAl%X;93IYh?sU37r^=L3n3WLQ28!QtLMFdWR&Bz?95S z0#br>Dyp63?4i}Wk2KFiqeX~Vt`oC*tqT#$6}^;`Hs> zOl(2&W|{S8OfKlpb!8-}F;IdDc;O8d^b4VYRq*i+^dx(X+vs2IgN|JwZ} z?!XCDbL+KrJr+Z}o;q|?kc~Vga|xFb$ztOZBdxxt%C){%J_C#QJ<3rr(PT*Jo^?Uf zJ@UR)6|dr%(*J6isF*C^7~bJljPD*ektfNY3fSn#*xYr{YmHpKKOo^^{<&ra9^H8fVs#_|8cqNXNW2L7St~=94@Dbd zS@J}#`Q(DrvDa3N7_GtM80j%T?-z8A$qH%9ms+=AO2GhV19Z^`T7N{~%522Av}w3L zb$U?KN~Tbz6`hS=DFMhe3&gz+Jdp7^&S^L9gqdYhYTx;N8s6CdI1)MgD-x)ecDNb$ zbYC3Q9+&sNUp^DB?R%u9v%ex6(Zk#ub8@$^2_oB-0yz{u>80!{bk=?GcT+?~ z$Sp0!5*%s^c%^Bi?0&J7f`sG zoH%YU_o}_cEvu3BsztID-)DGMh^L9~uwxJA?7kX1>o$8rgrbC*=-|nm^&*%btfKSq@{c&H9#pr74 z;~AeqRg=x@2^JQCkFI{L`5Moc-HHQ_J+SDgx{<)4;%z%7Wxj=>$v1?lF&5_KGo_=k zs^Xo9Y+HXZXtLyz-D_j+U4c{MFOH}H`nA3HbGPqR0Oi9bk*i-Oi&PM2<4^d_>>a*zWzqWS*-Yb6w33^Y3 zqy}&)E?5->r^F4y)hQ2h&zQgT`i5IbYugv065*nT!z*a^&DyHB@X>*JD76;Cq9dz@ z1`}!cS$5TpJJZ7h%ZYT)%!d4;!CZD~X339oPXuj`yLp^;+|=b^j82^yrkh7J>Jexk z9hi&7`=)pr`h*8ZRfVKnXjDqJr60~t9)o^~e?k{iUPSe~6ap-@ZpFqUA7g#hzfor0 z$peVUt(7m9PKjA-!9Cqy@lZkVwk{Dd6FtjBY5&@n&o=S#j0|Y96+n0Z*{@yrUGH;mT z0=4c+9CVc8H|wt`v2MnGTLJ17m7D#zVbC)ugS@d?mm7I;{V=}!E66bS3#&k7)QtMu z9xghM`{G2bp_Tg31iYeyvb~`TyT#N+9xk0Dt%#$pLs+suA1fgCE83i zMlbJ*Hfm2Rwd^}H^#78Yp%;^G>WzVkBXM5)aY&2F5uh?)B;(;N4{7ht+BF=Ts=q|6 z-q>U%)+|#j&|8!MQvl@N?u%L0WD|9$w@%U|23(2$1RBCdk$!SDch&!Za(gikvY;LY zipr7F6PW`Y%-T34>N_LNl!L6;LFf~IKC)uZ^q!vM>234%on49bRiA{-?uL`-W=pPo zfCTu$cBApv9wdxK5)pvp<&=UP`KY)UOB2Pq{(1T9|=G_i-CK4 z$>xph_y+?;g8S6&kyu-`v~7Va0VWqY)s%sIyT8gEJpRfe+}rm5d+&rMwuXGBkZ)R^ zuv=Q{lHkrOxdv-0KWuy8N-M&q*jW)>+xcPsku*WG71~n8qyLMM;4UZ`h1C%h+J$?m zEylUWK8OIMBU|fsAoi7BKsBxpe5l^E9HPL8ViYkOayMs7MBr}|PbG9BJ9@S|W`?JAx^YShMX zhdNa-b}*(lz(oXPg#Ap`$g`z4U{%Gxn-mxi1azx_D<3STF}q=PQ{V*1j6IEwei~EN z!+g5eMm<)kF@$~!jRahbjcqLH^!;VFpV{l%Q2ilx*Kg)QL-I;8Byf!3l=usI4q1k| zZ-cqL^sMJf$KtDsCI7d;^#_@9XmawiS*u|;wU^~A{^@2q6&;N|5U=kLa-tsDv^DNZ z9CnoPkhh|`@A+r1Z9D1}H;kUi&%(M87X^NJvXerQGax0VCvq)AIWL%F8O;Cv(w^I~ z?9jhBI8M(v-m6t5_t-_sndQX04#mZR8oJ1pr6^dUp%Xg_G_#y~+2o`>irdz?tJuLf z>N_u&5rHUv^-0M+32qb-h~iiOFOo2JVUNW4@&Et;07*qoM6N<$f>Le<82|tP literal 0 HcmV?d00001 diff --git a/HotSpot/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/HotSpot/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json index 9221b9b..fc07cee 100644 --- a/HotSpot/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/HotSpot/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,91 +1,109 @@ { "images" : [ { + "filename" : "40.png", "idiom" : "iphone", "scale" : "2x", "size" : "20x20" }, { + "filename" : "60.png", "idiom" : "iphone", "scale" : "3x", "size" : "20x20" }, { + "filename" : "58.png", "idiom" : "iphone", "scale" : "2x", "size" : "29x29" }, { + "filename" : "87.png", "idiom" : "iphone", "scale" : "3x", "size" : "29x29" }, { + "filename" : "80.png", "idiom" : "iphone", "scale" : "2x", "size" : "40x40" }, { + "filename" : "120.png", "idiom" : "iphone", "scale" : "3x", "size" : "40x40" }, { + "filename" : "120 1.png", "idiom" : "iphone", "scale" : "2x", "size" : "60x60" }, { + "filename" : "180.png", "idiom" : "iphone", "scale" : "3x", "size" : "60x60" }, { + "filename" : "20.png", "idiom" : "ipad", "scale" : "1x", "size" : "20x20" }, { + "filename" : "40 1.png", "idiom" : "ipad", "scale" : "2x", "size" : "20x20" }, { + "filename" : "29.png", "idiom" : "ipad", "scale" : "1x", "size" : "29x29" }, { + "filename" : "58 1.png", "idiom" : "ipad", "scale" : "2x", "size" : "29x29" }, { + "filename" : "40 2.png", "idiom" : "ipad", "scale" : "1x", "size" : "40x40" }, { + "filename" : "80 1.png", "idiom" : "ipad", "scale" : "2x", "size" : "40x40" }, { + "filename" : "76.png", "idiom" : "ipad", "scale" : "1x", "size" : "76x76" }, { + "filename" : "152.png", "idiom" : "ipad", "scale" : "2x", "size" : "76x76" }, { + "filename" : "167.png", "idiom" : "ipad", "scale" : "2x", "size" : "83.5x83.5" }, { + "filename" : "appstore.png", "idiom" : "ios-marketing", "scale" : "1x", "size" : "1024x1024" diff --git a/HotSpot/Resources/Assets.xcassets/AppIcon.appiconset/appstore.png b/HotSpot/Resources/Assets.xcassets/AppIcon.appiconset/appstore.png new file mode 100644 index 0000000000000000000000000000000000000000..58c7d4b04ca09b615d7c53fe732329f3d6959149 GIT binary patch literal 110805 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�BtD#VzCuiG^Tps+>P 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