From 7bc8559137660eed910627fba043c75c5e654f8b Mon Sep 17 00:00:00 2001 From: hooni Date: Sat, 25 Jan 2025 04:59:38 +0900 Subject: [PATCH 01/14] =?UTF-8?q?feat:=20#172=20=20=EA=B2=80=EC=83=89=20AP?= =?UTF-8?q?I=20=EB=B6=88=EB=9F=AC=EC=98=A4=EA=B8=B0=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Service/HomeService/HomeService.swift | 22 +++++++++++++++++++ .../Network/TargetType/HomeTargetType.swift | 5 +++++ .../Resource/Tab/NavigationManager.swift | 8 +++++-- .../Source/Feature/Home/HomeViewModel.swift | 13 +++++++++++ .../Source/Feature/Home/NMapView.swift | 22 +++++++++---------- .../Feature/Search/Models/SearchResult.swift | 1 + .../Source/Feature/Search/SearchStore.swift | 17 +++++++++----- .../Feature/Search/Views/SearchView.swift | 8 ++++--- 8 files changed, 74 insertions(+), 22 deletions(-) diff --git a/Spoony-iOS/Spoony-iOS/Network/Service/HomeService/HomeService.swift b/Spoony-iOS/Spoony-iOS/Network/Service/HomeService/HomeService.swift index a6a844a6..fb767004 100644 --- a/Spoony-iOS/Spoony-iOS/Network/Service/HomeService/HomeService.swift +++ b/Spoony-iOS/Spoony-iOS/Network/Service/HomeService/HomeService.swift @@ -12,6 +12,7 @@ protocol HomeServiceType { func fetchPickList(userId: Int) async throws -> ResturantpickListResponse func fetchSpoonCount(userId: Int) async throws -> Int func fetchFocusedPlace(userId: Int, placeId: Int) async throws -> MapFocusResponse + func fetchLocationList(userId: Int, locationId: Int) async throws -> ResturantpickListResponse } final class DefaultHomeService: HomeServiceType { @@ -83,4 +84,25 @@ final class DefaultHomeService: HomeServiceType { } } } + + func fetchLocationList(userId: Int, locationId: Int) async throws -> ResturantpickListResponse { + return try await withCheckedThrowingContinuation { continuation in + provider.request(.getLocationList(userId: userId, locationId: locationId)) { result in + switch result { + case .success(let response): + do { + let responseDto = try response.map(BaseResponse.self) + guard let data = responseDto.data else { + throw NSError(domain: "HomeService", code: -1, userInfo: [NSLocalizedDescriptionKey: "No data available"]) + } + continuation.resume(returning: data) + } catch { + continuation.resume(throwing: error) + } + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } } diff --git a/Spoony-iOS/Spoony-iOS/Network/TargetType/HomeTargetType.swift b/Spoony-iOS/Spoony-iOS/Network/TargetType/HomeTargetType.swift index ba126aaf..a91ea13d 100644 --- a/Spoony-iOS/Spoony-iOS/Network/TargetType/HomeTargetType.swift +++ b/Spoony-iOS/Spoony-iOS/Network/TargetType/HomeTargetType.swift @@ -14,6 +14,7 @@ enum HomeTargetType { case getMapFocus(userId: Int, placeId: Int) case getSearchResultList(query: String) case getSearchResultLocation(userId: Int, locationId: Int) + case getLocationList(userId: Int, locationId: Int) } extension HomeTargetType: TargetType { @@ -37,6 +38,8 @@ extension HomeTargetType: TargetType { return "/location/search" case .getSearchResultLocation(let userId, let locationId): return "/post/zzin/\(userId)/\(locationId)" + case .getLocationList(let userId, let locationId): + return "/post/zzim/location/\(userId)/\(locationId)" } } @@ -46,6 +49,7 @@ extension HomeTargetType: TargetType { .getMapList, .getMapFocus, .getSearchResultList, + .getLocationList, .getSearchResultLocation: return .get } @@ -56,6 +60,7 @@ extension HomeTargetType: TargetType { case .getSpoonCount, .getMapList, .getMapFocus, + .getLocationList, .getSearchResultLocation: return .requestPlain diff --git a/Spoony-iOS/Spoony-iOS/Resource/Tab/NavigationManager.swift b/Spoony-iOS/Spoony-iOS/Resource/Tab/NavigationManager.swift index 16239c63..72bac104 100644 --- a/Spoony-iOS/Spoony-iOS/Resource/Tab/NavigationManager.swift +++ b/Spoony-iOS/Spoony-iOS/Resource/Tab/NavigationManager.swift @@ -16,11 +16,15 @@ final class NavigationManager: ObservableObject { @Published var popup: PopupType? - @ViewBuilder + @MainActor @ViewBuilder func build(_ view: ViewType) -> some View { switch view { case .searchView: - SearchView() + if case .map = selectedTab { + SearchView(homeViewModel: HomeViewModel(service: DefaultHomeService())) + } else { + SearchView(homeViewModel: HomeViewModel(service: DefaultHomeService())) + } case .locationView: Home() case .detailView(let postId): diff --git a/Spoony-iOS/Spoony-iOS/Source/Feature/Home/HomeViewModel.swift b/Spoony-iOS/Spoony-iOS/Source/Feature/Home/HomeViewModel.swift index 1b2d42b3..f9c49900 100644 --- a/Spoony-iOS/Spoony-iOS/Source/Feature/Home/HomeViewModel.swift +++ b/Spoony-iOS/Spoony-iOS/Source/Feature/Home/HomeViewModel.swift @@ -50,6 +50,19 @@ final class HomeViewModel: ObservableObject { } } + func fetchLocationList(locationId: Int) { + Task { + isLoading = true + do { + let response = try await service.fetchLocationList(userId: Config.userId, locationId: locationId) + self.pickList = response.zzimCardResponses + } catch { + self.error = error + } + isLoading = false + } + } + func clearFocusedPlaces() { focusedPlaces = [] selectedLocation = nil diff --git a/Spoony-iOS/Spoony-iOS/Source/Feature/Home/NMapView.swift b/Spoony-iOS/Spoony-iOS/Source/Feature/Home/NMapView.swift index cdb8a1b1..7daa55f6 100644 --- a/Spoony-iOS/Spoony-iOS/Source/Feature/Home/NMapView.swift +++ b/Spoony-iOS/Spoony-iOS/Source/Feature/Home/NMapView.swift @@ -20,7 +20,7 @@ struct NMapView: UIViewRepresentable { private var mapView: NMFMapView let onMoveCamera: ((Double, Double) -> Void)? - + init(viewModel: HomeViewModel, selectedPlace: Binding, onMoveCamera: ((Double, Double) -> Void)? = nil) { @@ -42,22 +42,22 @@ struct NMapView: UIViewRepresentable { case .authorizedWhenInUse, .authorizedAlways: if let location = locationManager.location { moveCamera(mapView, to: NMGLatLng(lat: location.coordinate.latitude, - lng: location.coordinate.longitude)) + lng: location.coordinate.longitude)) } case .denied, .restricted: moveCamera(mapView, to: NMGLatLng(lat: defaultLatitude, lng: defaultLongitude)) case .notDetermined: locationManager.requestWhenInUseAuthorization() moveCamera(mapView, to: NMGLatLng(lat: defaultLatitude, lng: defaultLongitude)) - default: + default: moveCamera(mapView, to: NMGLatLng(lat: defaultLatitude, lng: defaultLongitude)) } } - - private func moveCamera(_ mapView: NMFMapView, to coord: NMGLatLng) { - let cameraUpdate = NMFCameraUpdate(scrollTo: coord) - mapView.moveCamera(cameraUpdate) - } + + private func moveCamera(_ mapView: NMFMapView, to coord: NMGLatLng) { + let cameraUpdate = NMFCameraUpdate(scrollTo: coord) + mapView.moveCamera(cameraUpdate) + } func makeCoordinator() -> Coordinator { Coordinator( @@ -80,7 +80,7 @@ struct NMapView: UIViewRepresentable { } context.coordinator.markers = newMarkers - + if !viewModel.pickList.isEmpty { let bounds = NMGLatLngBounds(latLngs: viewModel.pickList.map { NMGLatLng(lat: $0.latitude, lng: $0.longitude) @@ -100,7 +100,7 @@ struct NMapView: UIViewRepresentable { private func configureMapView(context: Context) -> NMFMapView { let mapView = NMFMapView() - mapView.positionMode = .direction + mapView.positionMode = .disabled mapView.zoomLevel = defaultZoomLevel mapView.touchDelegate = context.coordinator mapView.logoAlign = .rightTop @@ -114,7 +114,7 @@ struct NMapView: UIViewRepresentable { marker.captionColor = .black marker.captionTextSize = 14 -marker.captionMinZoom = isSelected ? 0 : 10 + marker.captionMinZoom = isSelected ? 0 : 10 marker.captionMaxZoom = 20 marker.anchor = CGPoint(x: 0.5, y: 1.0) diff --git a/Spoony-iOS/Spoony-iOS/Source/Feature/Search/Models/SearchResult.swift b/Spoony-iOS/Spoony-iOS/Source/Feature/Search/Models/SearchResult.swift index 962d0b84..19635d98 100644 --- a/Spoony-iOS/Spoony-iOS/Source/Feature/Search/Models/SearchResult.swift +++ b/Spoony-iOS/Spoony-iOS/Source/Feature/Search/Models/SearchResult.swift @@ -10,5 +10,6 @@ import Foundation struct SearchResult: Identifiable, Equatable { let id = UUID() let title: String + let locationId: Int let address: String } diff --git a/Spoony-iOS/Spoony-iOS/Source/Feature/Search/SearchStore.swift b/Spoony-iOS/Spoony-iOS/Source/Feature/Search/SearchStore.swift index 837f228d..8ef2a19a 100644 --- a/Spoony-iOS/Spoony-iOS/Source/Feature/Search/SearchStore.swift +++ b/Spoony-iOS/Spoony-iOS/Source/Feature/Search/SearchStore.swift @@ -14,12 +14,14 @@ final class SearchStore: ObservableObject { private let searchService: SearchService private var navigationManager: NavigationManager + private let homeViewModel: HomeViewModel - init(navigationManager: NavigationManager) { - self.model = SearchModel() - self.searchService = SearchService() - self.navigationManager = navigationManager - } + init(navigationManager: NavigationManager, homeViewModel: HomeViewModel) { + self.model = SearchModel() + self.searchService = SearchService() + self.navigationManager = navigationManager + self.homeViewModel = homeViewModel + } func dispatch(_ intent: SearchIntent) { switch intent { @@ -82,6 +84,9 @@ final class SearchStore: ObservableObject { private func handleLocationSelection(_ result: SearchResult) { navigationManager.currentLocation = result.title + Task { + await homeViewModel.fetchLocationList(locationId: result.locationId) + } navigationManager.pop(1) } @@ -96,7 +101,7 @@ final class SearchStore: ObservableObject { let response = try await searchService.searchLocation(query: query) let results = response.locationResponseList.map { location in SearchResult( - title: location.locationName, + title: location.locationName, locationId: location.locationId, address: location.locationAddress ?? "" ) } diff --git a/Spoony-iOS/Spoony-iOS/Source/Feature/Search/Views/SearchView.swift b/Spoony-iOS/Spoony-iOS/Source/Feature/Search/Views/SearchView.swift index 8d5fe077..ad3a5469 100644 --- a/Spoony-iOS/Spoony-iOS/Source/Feature/Search/Views/SearchView.swift +++ b/Spoony-iOS/Spoony-iOS/Source/Feature/Search/Views/SearchView.swift @@ -11,14 +11,16 @@ struct SearchView: View { @EnvironmentObject private var navigationManager: NavigationManager @StateObject private var store: SearchStore @FocusState private var isSearchFocused: Bool + let homeViewModel: HomeViewModel - init() { + init(homeViewModel: HomeViewModel) { + self.homeViewModel = homeViewModel let tempNavigationManager = NavigationManager() - _store = StateObject(wrappedValue: SearchStore(navigationManager: tempNavigationManager)) + _store = StateObject(wrappedValue: SearchStore(navigationManager: tempNavigationManager, homeViewModel: homeViewModel)) } private var initStore: SearchStore { - SearchStore(navigationManager: navigationManager) + SearchStore(navigationManager: navigationManager, homeViewModel: HomeViewModel()) } var body: some View { From 93d6feff349319562e8af6d65784901db00f7a88 Mon Sep 17 00:00:00 2001 From: hooni Date: Wed, 29 Jan 2025 20:15:44 +0900 Subject: [PATCH 02/14] =?UTF-8?q?feat:=20#172=20=EC=B9=B4=EB=A9=94?= =?UTF-8?q?=EB=9D=BC=20=EC=8B=9C=EC=A0=90=20=EA=B3=A0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Spoony-iOS/Source/Feature/Home/HomeViewModel.swift | 3 +-- .../Spoony-iOS/Source/Feature/Home/NMapView.swift | 10 +++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Spoony-iOS/Spoony-iOS/Source/Feature/Home/HomeViewModel.swift b/Spoony-iOS/Spoony-iOS/Source/Feature/Home/HomeViewModel.swift index f9c49900..a330e570 100644 --- a/Spoony-iOS/Spoony-iOS/Source/Feature/Home/HomeViewModel.swift +++ b/Spoony-iOS/Spoony-iOS/Source/Feature/Home/HomeViewModel.swift @@ -65,6 +65,5 @@ final class HomeViewModel: ObservableObject { func clearFocusedPlaces() { focusedPlaces = [] - selectedLocation = nil - } + } } diff --git a/Spoony-iOS/Spoony-iOS/Source/Feature/Home/NMapView.swift b/Spoony-iOS/Spoony-iOS/Source/Feature/Home/NMapView.swift index 7daa55f6..af08cfc7 100644 --- a/Spoony-iOS/Spoony-iOS/Source/Feature/Home/NMapView.swift +++ b/Spoony-iOS/Spoony-iOS/Source/Feature/Home/NMapView.swift @@ -79,16 +79,17 @@ struct NMapView: UIViewRepresentable { return marker } - context.coordinator.markers = newMarkers - - if !viewModel.pickList.isEmpty { + // 처음 지도가 로드될 때만 모든 마커가 보이도록 카메라 이동 + if context.coordinator.isInitialLoad && !viewModel.pickList.isEmpty { let bounds = NMGLatLngBounds(latLngs: viewModel.pickList.map { NMGLatLng(lat: $0.latitude, lng: $0.longitude) }) let cameraUpdate = NMFCameraUpdate(fit: bounds, padding: 50) mapView.moveCamera(cameraUpdate) + context.coordinator.isInitialLoad = false } + // 마커가 선택됐을 때만 해당 위치로 카메라 이동 if let location = viewModel.selectedLocation { let coord = NMGLatLng(lat: location.latitude, lng: location.longitude) let cameraUpdate = NMFCameraUpdate(scrollTo: coord) @@ -96,6 +97,8 @@ struct NMapView: UIViewRepresentable { cameraUpdate.animationDuration = 0.2 mapView.moveCamera(cameraUpdate) } + + context.coordinator.markers = newMarkers } private func configureMapView(context: Context) -> NMFMapView { @@ -171,6 +174,7 @@ struct NMapView: UIViewRepresentable { final class Coordinator: NSObject, NMFMapViewTouchDelegate { @Binding var selectedPlace: CardPlace? var markers: [NMFMarker] = [] + var isInitialLoad = true private let defaultMarkerImage: NMFOverlayImage private let viewModel: HomeViewModel From 2a0a6c8ba9d0125ce1c0438e4e7463049c23a2e6 Mon Sep 17 00:00:00 2001 From: hooni Date: Wed, 29 Jan 2025 23:09:26 +0900 Subject: [PATCH 03/14] =?UTF-8?q?feat:=20#172=20=EC=B9=B4=EB=A9=94?= =?UTF-8?q?=EB=9D=BC=20=EC=8B=9C=EC=A0=90=20=EA=B3=A0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 카메라 시점 해제시 그위치 그대로 --- .../Spoony-iOS/Source/Feature/Home/NMapView.swift | 2 +- .../Feature/Search/Models/SearchResult.swift | 15 --------------- 2 files changed, 1 insertion(+), 16 deletions(-) delete mode 100644 Spoony-iOS/Spoony-iOS/Source/Feature/Search/Models/SearchResult.swift diff --git a/Spoony-iOS/Spoony-iOS/Source/Feature/Home/NMapView.swift b/Spoony-iOS/Spoony-iOS/Source/Feature/Home/NMapView.swift index af08cfc7..d6aba588 100644 --- a/Spoony-iOS/Spoony-iOS/Source/Feature/Home/NMapView.swift +++ b/Spoony-iOS/Spoony-iOS/Source/Feature/Home/NMapView.swift @@ -174,7 +174,7 @@ struct NMapView: UIViewRepresentable { final class Coordinator: NSObject, NMFMapViewTouchDelegate { @Binding var selectedPlace: CardPlace? var markers: [NMFMarker] = [] - var isInitialLoad = true + var isInitialLoad: Bool = true private let defaultMarkerImage: NMFOverlayImage private let viewModel: HomeViewModel diff --git a/Spoony-iOS/Spoony-iOS/Source/Feature/Search/Models/SearchResult.swift b/Spoony-iOS/Spoony-iOS/Source/Feature/Search/Models/SearchResult.swift deleted file mode 100644 index 19635d98..00000000 --- a/Spoony-iOS/Spoony-iOS/Source/Feature/Search/Models/SearchResult.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// SearchResult.swift -// Spoony-iOS -// -// Created by 이지훈 on 1/16/25. -// - -import Foundation - -struct SearchResult: Identifiable, Equatable { - let id = UUID() - let title: String - let locationId: Int - let address: String -} From 00680d7424f386994fb98010863af14180d03c64 Mon Sep 17 00:00:00 2001 From: hooni Date: Thu, 30 Jan 2025 14:07:43 +0900 Subject: [PATCH 04/14] =?UTF-8?q?feat:=20#172=20=EB=A7=88=EC=BB=A4=20?= =?UTF-8?q?=EB=90=98=EB=8F=8C=EB=A6=AC=EA=B8=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Model/ResturantpickListResponse.swift | 16 ++++- .../Network/Model/SearchResult.swift | 37 +++++++++++ .../Network/TargetType/HomeTargetType.swift | 2 +- .../BottomSheet/BottomSheetList.swift | 19 ++++-- .../Spoony-iOS/Source/Feature/Home/Home.swift | 30 ++++----- .../Source/Feature/Home/HomeViewModel.swift | 64 +++++++++++-------- .../Source/Feature/Home/NMapView.swift | 4 +- .../Source/Feature/Search/SearchStore.swift | 25 +++++--- 8 files changed, 136 insertions(+), 61 deletions(-) create mode 100644 Spoony-iOS/Spoony-iOS/Network/Model/SearchResult.swift diff --git a/Spoony-iOS/Spoony-iOS/Network/Model/ResturantpickListResponse.swift b/Spoony-iOS/Spoony-iOS/Network/Model/ResturantpickListResponse.swift index 9a7756be..a629a030 100644 --- a/Spoony-iOS/Spoony-iOS/Network/Model/ResturantpickListResponse.swift +++ b/Spoony-iOS/Spoony-iOS/Network/Model/ResturantpickListResponse.swift @@ -11,7 +11,7 @@ struct ResturantpickListResponse: Codable { let zzimCardResponses: [PickListCardResponse] } -struct PickListCardResponse: Codable { +struct PickListCardResponse: Codable, Equatable { let placeId: Int let placeName: String let placeAddress: String @@ -20,9 +20,21 @@ struct PickListCardResponse: Codable { let latitude: Double let longitude: Double let categoryColorResponse: BottomSheetCategoryColorResponse + + static func == (lhs: PickListCardResponse, rhs: PickListCardResponse) -> Bool { + return lhs.placeId == rhs.placeId && + lhs.placeName == rhs.placeName && + lhs.placeAddress == rhs.placeAddress && + lhs.postTitle == rhs.postTitle && + lhs.photoUrl == rhs.photoUrl && + lhs.latitude == rhs.latitude && + lhs.longitude == rhs.longitude && + lhs.categoryColorResponse == rhs.categoryColorResponse + } } -struct BottomSheetCategoryColorResponse: Codable { +struct BottomSheetCategoryColorResponse: Codable, Equatable { + let categoryId: Int let categoryName: String let iconUrl: String let iconTextColor: String diff --git a/Spoony-iOS/Spoony-iOS/Network/Model/SearchResult.swift b/Spoony-iOS/Spoony-iOS/Network/Model/SearchResult.swift new file mode 100644 index 00000000..c18b3dc2 --- /dev/null +++ b/Spoony-iOS/Spoony-iOS/Network/Model/SearchResult.swift @@ -0,0 +1,37 @@ +// +// SearchResult.swift +// Spoony-iOS +// +// Created by 이지훈 on 1/16/25. +// + +import Foundation + +struct SearchResult: Identifiable, Equatable { + let id = UUID() + let title: String + let locationId: Int + let address: String +} + +struct Location: Equatable { + let title: String + let id: Int + let latitude: Double + let longitude: Double + + init(title: String, id: Int, latitude: Double, longitude: Double) { + self.title = title + self.id = id + self.latitude = latitude + self.longitude = longitude + } + + init(searchResult: SearchResult) { + self.title = searchResult.title + self.id = searchResult.locationId + // 검색 결과에서는 기본 위치 사용 + self.latitude = 37.5666103 // 서울시청 위도 + self.longitude = 126.9783882 // 서울시청 경도 + } +} diff --git a/Spoony-iOS/Spoony-iOS/Network/TargetType/HomeTargetType.swift b/Spoony-iOS/Spoony-iOS/Network/TargetType/HomeTargetType.swift index a91ea13d..5fe107ab 100644 --- a/Spoony-iOS/Spoony-iOS/Network/TargetType/HomeTargetType.swift +++ b/Spoony-iOS/Spoony-iOS/Network/TargetType/HomeTargetType.swift @@ -37,7 +37,7 @@ extension HomeTargetType: TargetType { case .getSearchResultList: return "/location/search" case .getSearchResultLocation(let userId, let locationId): - return "/post/zzin/\(userId)/\(locationId)" + return "/post/zzim/\(userId)/\(locationId)" case .getLocationList(let userId, let locationId): return "/post/zzim/location/\(userId)/\(locationId)" } diff --git a/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/BottomSheetList.swift b/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/BottomSheetList.swift index b50e6c73..7406ad19 100644 --- a/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/BottomSheetList.swift +++ b/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/BottomSheetList.swift @@ -142,13 +142,18 @@ struct BottomSheetListView: View { .padding(.top, 10) HStack(spacing: 4) { - Text("양수정님의 찐맛집") - .customFont(.body2b) - Text("\(viewModel.pickList.count)") - .customFont(.body2b) - .foregroundColor(.gray500) - } - .padding(.bottom, 8) + if !viewModel.focusedPlaces.isEmpty { + Text("상세 정보") + .customFont(.body2b) + } else { + Text("양수정님의 찐맛집") + .customFont(.body2b) + Text("\(viewModel.pickList.count)") + .customFont(.body2b) + .foregroundColor(.gray500) + } + } + .padding(.bottom, 8) } .frame(height: 60.adjustedH) .background(.white) diff --git a/Spoony-iOS/Spoony-iOS/Source/Feature/Home/Home.swift b/Spoony-iOS/Spoony-iOS/Source/Feature/Home/Home.swift index 5c1da8d8..093640a3 100644 --- a/Spoony-iOS/Spoony-iOS/Source/Feature/Home/Home.swift +++ b/Spoony-iOS/Spoony-iOS/Source/Feature/Home/Home.swift @@ -7,7 +7,6 @@ import SwiftUI - struct Home: View { @EnvironmentObject var navigationManager: NavigationManager @StateObject private var viewModel = HomeViewModel(service: DefaultHomeService()) @@ -34,9 +33,11 @@ struct Home: View { selectedPlace = newPlaces[0] } } - .onChange(of: selectedPlace) { _, newPlace in - if newPlace == nil { - viewModel.clearFocusedPlaces() + .onChange(of: viewModel.pickList) { _ in + // Reset selection when new search results arrive + selectedPlace = nil + if let location = viewModel.selectedLocation { + print("Moving to new location: \(location)") } } @@ -47,6 +48,7 @@ struct Home: View { title: locationTitle, searchText: $searchText, onBackTapped: { + viewModel.fetchPickList() // Fetch original list when going back navigationManager.currentLocation = nil } ) @@ -74,28 +76,26 @@ struct Home: View { .padding(.bottom, 12) .transition(.move(edge: .bottom)) } else { - if navigationManager.currentLocation != nil { - BottomSheetListView(viewModel: viewModel) - } else if !viewModel.pickList.isEmpty { + if !viewModel.pickList.isEmpty { BottomSheetListView(viewModel: viewModel) } else { FixedBottomSheetView() } } } - } + } .navigationBarHidden(true) .task { isBottomSheetPresented = true - Task { - do { - spoonCount = try await restaurantService.fetchSpoonCount(userId: Config.userId) - } catch { - print("Failed to fetch spoon count:", error) + do { + spoonCount = try await restaurantService.fetchSpoonCount(userId: Config.userId) + // Only fetch initial list if not in search results + if navigationManager.currentLocation == nil { + viewModel.fetchPickList() } - viewModel.fetchPickList() + } catch { + print("Failed to fetch spoon count:", error) } - viewModel.fetchPickList() } } } diff --git a/Spoony-iOS/Spoony-iOS/Source/Feature/Home/HomeViewModel.swift b/Spoony-iOS/Spoony-iOS/Source/Feature/Home/HomeViewModel.swift index a330e570..07555a32 100644 --- a/Spoony-iOS/Spoony-iOS/Source/Feature/Home/HomeViewModel.swift +++ b/Spoony-iOS/Spoony-iOS/Source/Feature/Home/HomeViewModel.swift @@ -34,36 +34,50 @@ final class HomeViewModel: ObservableObject { } func fetchFocusedPlace(placeId: Int) { - Task { - isLoading = true - do { - if let selectedPlace = pickList.first(where: { $0.placeId == placeId }) { - selectedLocation = (selectedPlace.latitude, selectedPlace.longitude) - } - - let response = try await service.fetchFocusedPlace(userId: Config.userId, placeId: placeId) - self.focusedPlaces = response.zzimFocusResponseList.map { $0.toCardPlace() } - } catch { - self.error = error + Task { + isLoading = true + do { + if let selectedPlace = pickList.first(where: { $0.placeId == placeId }) { + selectedLocation = (selectedPlace.latitude, selectedPlace.longitude) } - isLoading = false + + let response = try await service.fetchFocusedPlace(userId: Config.userId, placeId: placeId) + self.focusedPlaces = response.zzimFocusResponseList.map { $0.toCardPlace() } + } catch { + self.error = error } + isLoading = false } + } - func fetchLocationList(locationId: Int) { - Task { - isLoading = true - do { - let response = try await service.fetchLocationList(userId: Config.userId, locationId: locationId) - self.pickList = response.zzimCardResponses - } catch { - self.error = error - } - isLoading = false - } - } + @MainActor + func fetchLocationList(locationId: Int) async { + isLoading = true + do { + // Clear existing data first + clearFocusedPlaces() + selectedLocation = nil + pickList = [] + + let response = try await service.fetchLocationList(userId: Config.userId, locationId: locationId) + + // Set new data + await MainActor.run { + self.pickList = response.zzimCardResponses + + if let firstPlace = response.zzimCardResponses.first { + self.selectedLocation = (firstPlace.latitude, firstPlace.longitude) + } + } + } catch { + self.error = error + print("Error in fetchLocationList:", error) + } + isLoading = false + } + func clearFocusedPlaces() { - focusedPlaces = [] + focusedPlaces = [] } } diff --git a/Spoony-iOS/Spoony-iOS/Source/Feature/Home/NMapView.swift b/Spoony-iOS/Spoony-iOS/Source/Feature/Home/NMapView.swift index d6aba588..611e77be 100644 --- a/Spoony-iOS/Spoony-iOS/Source/Feature/Home/NMapView.swift +++ b/Spoony-iOS/Spoony-iOS/Source/Feature/Home/NMapView.swift @@ -146,7 +146,6 @@ struct NMapView: UIViewRepresentable { marker.iconImage = defaultMarker } - // 모든 마커에 캡션 설정 configureMarkerCaption(marker, with: pickCard.placeName, isSelected: isSelected) marker.touchHandler = { [weak viewModel, weak marker] (_) -> Bool in @@ -162,12 +161,13 @@ struct NMapView: UIViewRepresentable { resetMarker(marker) marker.mapView = mapView selectedPlace = nil + viewModel?.clearFocusedPlaces() } return true } - return marker + return marker // 마커 반환 추가 } } diff --git a/Spoony-iOS/Spoony-iOS/Source/Feature/Search/SearchStore.swift b/Spoony-iOS/Spoony-iOS/Source/Feature/Search/SearchStore.swift index 8ef2a19a..03b24de8 100644 --- a/Spoony-iOS/Spoony-iOS/Source/Feature/Search/SearchStore.swift +++ b/Spoony-iOS/Spoony-iOS/Source/Feature/Search/SearchStore.swift @@ -21,7 +21,7 @@ final class SearchStore: ObservableObject { self.searchService = SearchService() self.navigationManager = navigationManager self.homeViewModel = homeViewModel - } + } func dispatch(_ intent: SearchIntent) { switch intent { @@ -49,7 +49,6 @@ final class SearchStore: ObservableObject { if normalizedText.isEmpty { state = .empty } else { - state = .typing(searchText: normalizedText) } } @@ -59,7 +58,7 @@ final class SearchStore: ObservableObject { let normalizedSearchText = model.searchText.components(separatedBy: .whitespaces) .filter { !$0.isEmpty } - .joined(separator: "") + .joined(separator: " ") state = .loading updateSearchResults(with: normalizedSearchText) @@ -83,11 +82,17 @@ final class SearchStore: ObservableObject { } private func handleLocationSelection(_ result: SearchResult) { - navigationManager.currentLocation = result.title Task { - await homeViewModel.fetchLocationList(locationId: result.locationId) + do { + await homeViewModel.fetchLocationList(locationId: result.locationId) + await MainActor.run { + navigationManager.currentLocation = result.title + navigationManager.pop(1) + } + } catch { + print("Failed to fetch location list:", error) + } } - navigationManager.pop(1) } private func updateSearchResults(with query: String) { @@ -101,7 +106,8 @@ final class SearchStore: ObservableObject { let response = try await searchService.searchLocation(query: query) let results = response.locationResponseList.map { location in SearchResult( - title: location.locationName, locationId: location.locationId, + title: location.locationName, + locationId: location.locationId, address: location.locationAddress ?? "" ) } @@ -126,9 +132,10 @@ final class SearchStore: ObservableObject { } } } + func updateNavigationManager(_ manager: NavigationManager) { - navigationManager = manager - } + navigationManager = manager + } private func saveRecentSearches() { UserManager.shared.recentSearches = model.recentSearches From 173a39d4551a6f86b72398cff1957a18e2f8bb4a Mon Sep 17 00:00:00 2001 From: hooni Date: Thu, 30 Jan 2025 20:52:52 +0900 Subject: [PATCH 05/14] =?UTF-8?q?feat:=20#172=20=EB=B0=94=ED=85=80?= =?UTF-8?q?=EC=8B=9C=ED=8A=B8=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Model/SearchLocationResponse.swift | 45 ++++++++ .../Network/Model/SearchResult.swift | 42 ++++--- .../BottomSheet/Plain/BottomSheetItem.swift | 103 +++++++++++++++++ .../{ => Plain}/BottomSheetList.swift | 95 ---------------- .../SearchResult/SearchLocationCardItem.swift | 104 ++++++++++++++++++ .../SearchResultBottomSheet.swift | 102 +++++++++++++++++ 6 files changed, 378 insertions(+), 113 deletions(-) create mode 100644 Spoony-iOS/Spoony-iOS/Network/Model/SearchLocationResponse.swift create mode 100644 Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/Plain/BottomSheetItem.swift rename Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/{ => Plain}/BottomSheetList.swift (66%) create mode 100644 Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/SearchResult/SearchLocationCardItem.swift create mode 100644 Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/SearchResult/SearchResultBottomSheet.swift diff --git a/Spoony-iOS/Spoony-iOS/Network/Model/SearchLocationResponse.swift b/Spoony-iOS/Spoony-iOS/Network/Model/SearchLocationResponse.swift new file mode 100644 index 00000000..e49fde3f --- /dev/null +++ b/Spoony-iOS/Spoony-iOS/Network/Model/SearchLocationResponse.swift @@ -0,0 +1,45 @@ +// +// SearchLocationResponse.swift +// Spoony-iOS +// +// Created by 이지훈 on 1/30/25. +// + +import Foundation + +struct SearchLocationResponse: Codable { + let success: Bool + let data: SearchLocationData? + let error: String? +} + +struct SearchLocationData: Codable { + let zzimCardResponses: [SearchLocationResult] +} + +struct SearchLocationResult: Codable, Identifiable { + var id = UUID() + let locationId: Int + let placeId: Int? + let title: String + let address: String + let postTitle: String? + let photoUrl: String? + let latitude: Double? + let longitude: Double? + let categoryColorResponse: SearchCategoryColorResponse? +} + +struct SearchCategoryColorResponse: Codable { + let categoryId: Int + let categoryName: String + let iconUrl: String + let iconTextColor: String + let iconBackgroundColor: String +} + +extension SearchLocationResponse { + func toEntity() -> [SearchLocationResult] { + return data?.zzimCardResponses ?? [] + } +} diff --git a/Spoony-iOS/Spoony-iOS/Network/Model/SearchResult.swift b/Spoony-iOS/Spoony-iOS/Network/Model/SearchResult.swift index c18b3dc2..6794d585 100644 --- a/Spoony-iOS/Spoony-iOS/Network/Model/SearchResult.swift +++ b/Spoony-iOS/Spoony-iOS/Network/Model/SearchResult.swift @@ -14,24 +14,30 @@ struct SearchResult: Identifiable, Equatable { let address: String } -struct Location: Equatable { - let title: String - let id: Int - let latitude: Double - let longitude: Double - - init(title: String, id: Int, latitude: Double, longitude: Double) { - self.title = title - self.id = id - self.latitude = latitude - self.longitude = longitude +extension PickListCardResponse { + func toSearchLocationResult() -> SearchLocationResult { + return SearchLocationResult( + locationId: self.placeId, + placeId: self.placeId, + title: self.placeName, + address: self.placeAddress, + postTitle: self.postTitle, + photoUrl: self.photoUrl, + latitude: self.latitude, + longitude: self.longitude, + categoryColorResponse: self.categoryColorResponse.toSearchCategoryColorResponse() + ) } - - init(searchResult: SearchResult) { - self.title = searchResult.title - self.id = searchResult.locationId - // 검색 결과에서는 기본 위치 사용 - self.latitude = 37.5666103 // 서울시청 위도 - self.longitude = 126.9783882 // 서울시청 경도 +} + +extension BottomSheetCategoryColorResponse { + func toSearchCategoryColorResponse() -> SearchCategoryColorResponse { + return SearchCategoryColorResponse( + categoryId: self.categoryId, + categoryName: self.categoryName, + iconUrl: self.iconUrl, + iconTextColor: self.iconTextColor, + iconBackgroundColor: self.iconBackgroundColor + ) } } diff --git a/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/Plain/BottomSheetItem.swift b/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/Plain/BottomSheetItem.swift new file mode 100644 index 00000000..3b9a3c3f --- /dev/null +++ b/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/Plain/BottomSheetItem.swift @@ -0,0 +1,103 @@ +// +// BottomSheetItem.swift +// Spoony-iOS +// +// Created by 이지훈 on 1/30/25. +// + +import SwiftUI + +struct BottomSheetListItem: View { + let pickCard: PickListCardResponse + + var body: some View { + HStack(spacing: 12) { + VStack(alignment: .leading, spacing: 8) { + HStack(spacing: 8) { + Text(pickCard.placeName) + .customFont(.body1b) + .lineLimit(1) + .truncationMode(.tail) + + // 카테고리 칩 + HStack(spacing: 4) { + // 카테고리 아이콘 + AsyncImage(url: URL(string: pickCard.categoryColorResponse.iconUrl)) { phase in + switch phase { + case .success(let image): + image + .resizable() + .scaledToFit() + .frame(width: 16, height: 16) + default: + Color.clear + .frame(width: 16, height: 16) + } + } + + Text(pickCard.categoryColorResponse.categoryName) + .customFont(.caption1m) + .lineLimit(1) + } + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Color(hex: pickCard.categoryColorResponse.iconBackgroundColor)) + .foregroundColor(Color(hex: pickCard.categoryColorResponse.iconTextColor)) + .cornerRadius(12) + } + + Text(pickCard.placeAddress) + .customFont(.caption1m) + .foregroundColor(.gray600) + .lineLimit(1) + .truncationMode(.tail) + + Text(pickCard.postTitle) + .customFont(.caption1m) + .foregroundColor(.spoonBlack) + .lineLimit(1) + .truncationMode(.tail) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.vertical, 12) + .padding(.horizontal, 12) + .background(.white) + .cornerRadius(8) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Color(.gray0), lineWidth: 1) + ) + .shadow( + color: Color(.gray0), + radius: 16, + x: 0, + y: 2 + ) + } + // 이미지 + AsyncImage(url: URL(string: pickCard.photoUrl)) { phase in + switch phase { + case .success(let image): + image + .resizable() + .scaledToFill() + case .failure: + defaultPlaceholder + case .empty: + defaultPlaceholder + @unknown default: + defaultPlaceholder + } + } + .frame(width: 98.adjusted, height: 98.adjusted) + .clipShape(RoundedRectangle(cornerRadius: 8)) + .layoutPriority(0) + } + .padding(.horizontal, 16) + .frame(height: 120.adjusted) + } + + private var defaultPlaceholder: some View { + RoundedRectangle(cornerRadius: 8) + .fill(Color.gray.opacity(0.1)) + } +} diff --git a/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/BottomSheetList.swift b/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/Plain/BottomSheetList.swift similarity index 66% rename from Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/BottomSheetList.swift rename to Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/Plain/BottomSheetList.swift index 7406ad19..f1223216 100644 --- a/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/BottomSheetList.swift +++ b/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/Plain/BottomSheetList.swift @@ -7,101 +7,6 @@ import SwiftUI -struct BottomSheetListItem: View { - let pickCard: PickListCardResponse - - var body: some View { - HStack(spacing: 12) { - VStack(alignment: .leading, spacing: 8) { - HStack(spacing: 8) { - Text(pickCard.placeName) - .customFont(.body1b) - .lineLimit(1) - .truncationMode(.tail) - - // 카테고리 칩 - HStack(spacing: 4) { - // 카테고리 아이콘 - AsyncImage(url: URL(string: pickCard.categoryColorResponse.iconUrl)) { phase in - switch phase { - case .success(let image): - image - .resizable() - .scaledToFit() - .frame(width: 16, height: 16) - default: - Color.clear - .frame(width: 16, height: 16) - } - } - - Text(pickCard.categoryColorResponse.categoryName) - .customFont(.caption1m) - .lineLimit(1) - } - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(Color(hex: pickCard.categoryColorResponse.iconBackgroundColor)) - .foregroundColor(Color(hex: pickCard.categoryColorResponse.iconTextColor)) - .cornerRadius(12) - } - - Text(pickCard.placeAddress) - .customFont(.caption1m) - .foregroundColor(.gray600) - .lineLimit(1) - .truncationMode(.tail) - - Text(pickCard.postTitle) - .customFont(.caption1m) - .foregroundColor(.spoonBlack) - .lineLimit(1) - .truncationMode(.tail) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.vertical, 12) - .padding(.horizontal, 12) - .background(.white) - .cornerRadius(8) - .overlay( - RoundedRectangle(cornerRadius: 8) - .stroke(Color(.gray0), lineWidth: 1) - ) - .shadow( - color: Color(.gray0), - radius: 16, - x: 0, - y: 2 - ) - } - // 이미지 - AsyncImage(url: URL(string: pickCard.photoUrl)) { phase in - switch phase { - case .success(let image): - image - .resizable() - .scaledToFill() - case .failure: - defaultPlaceholder - case .empty: - defaultPlaceholder - @unknown default: - defaultPlaceholder - } - } - .frame(width: 98.adjusted, height: 98.adjusted) - .clipShape(RoundedRectangle(cornerRadius: 8)) - .layoutPriority(0) - } - .padding(.horizontal, 16) - .frame(height: 120.adjusted) - } - - private var defaultPlaceholder: some View { - RoundedRectangle(cornerRadius: 8) - .fill(Color.gray.opacity(0.1)) - } -} - struct BottomSheetListView: View { @ObservedObject var viewModel: HomeViewModel @State private var currentStyle: BottomSheetStyle = .minimal diff --git a/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/SearchResult/SearchLocationCardItem.swift b/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/SearchResult/SearchLocationCardItem.swift new file mode 100644 index 00000000..b9e7def9 --- /dev/null +++ b/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/SearchResult/SearchLocationCardItem.swift @@ -0,0 +1,104 @@ +// +// SearchLocationCardItem.swift +// Spoony-iOS +// +// Created by 이지훈 on 1/30/25. +// + +import SwiftUI + +struct SearchLocationCardItem: View { + let pickCard: SearchLocationResult + + var body: some View { + HStack(spacing: 12) { + VStack(alignment: .leading, spacing: 8) { + HStack(spacing: 8) { + Text(pickCard.title) + .customFont(.body1b) + .lineLimit(1) + .truncationMode(.tail) + + if let category = pickCard.categoryColorResponse { + HStack(spacing: 4) { + AsyncImage(url: URL(string: category.iconUrl)) { phase in + switch phase { + case .success(let image): + image + .resizable() + .scaledToFit() + .frame(width: 16, height: 16) + default: + Color.clear + .frame(width: 16, height: 16) + } + } + + Text(category.categoryName) + .customFont(.caption1m) + .lineLimit(1) + } + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Color(hex: category.iconBackgroundColor)) + .foregroundColor(Color(hex: category.iconTextColor)) + .cornerRadius(12) + } + } + + Text(pickCard.address) + .customFont(.caption1m) + .foregroundColor(.gray600) + .lineLimit(1) + .truncationMode(.tail) + + if let postTitle = pickCard.postTitle { + Text(postTitle) + .customFont(.caption1m) + .foregroundColor(.spoonBlack) + .lineLimit(1) + .truncationMode(.tail) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.vertical, 12) + .padding(.horizontal, 12) + .background(.white) + .cornerRadius(8) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Color(.gray0), lineWidth: 1) + ) + .shadow( + color: Color(.gray0), + radius: 16, + x: 0, + y: 2 + ) + } + } + AsyncImage(url: URL(string: pickCard.photoUrl ?? "")) { phase in + switch phase { + case .success(let image): + image + .resizable() + .scaledToFill() + case .failure: + defaultPlaceholder + case .empty: + defaultPlaceholder + @unknown default: + defaultPlaceholder + } + } + .frame(width: 98.adjusted, height: 98.adjusted) + .clipShape(RoundedRectangle(cornerRadius: 8)) + .layoutPriority(0) + } + .padding(.horizontal, 16) + .frame(height: 120.adjusted) + } + + private var defaultPlaceholder: some View { + RoundedRectangle(cornerRadius: 8) + .fill(Color.gray.opacity(0.1)) + } +} diff --git a/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/SearchResult/SearchResultBottomSheet.swift b/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/SearchResult/SearchResultBottomSheet.swift new file mode 100644 index 00000000..7c0f81ac --- /dev/null +++ b/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/SearchResult/SearchResultBottomSheet.swift @@ -0,0 +1,102 @@ +// +// SearchLocationBottomSheetView.swift +// Spoony-iOS +// +// Created by 이지훈 on 1/30/25. +// + +import SwiftUI + +struct SearchLocationBottomSheetView: View { + @ObservedObject var viewModel: HomeViewModel + @State private var currentStyle: BottomSheetStyle = .minimal + @State private var offset: CGFloat = 0 + @GestureState private var isDragging: Bool = false + @State private var scrollOffset: CGFloat = 0 + @State private var isScrollEnabled: Bool = false + + private var snapPoints: [CGFloat] { + [ + BottomSheetStyle.minimal.height, + BottomSheetStyle.half.height, + BottomSheetStyle.full.height + ] + } + + private func getClosestSnapPoint(to offset: CGFloat) -> BottomSheetStyle { + let screenHeight = UIScreen.main.bounds.height + let currentHeight = screenHeight - offset + + let distances = [ + (abs(currentHeight - BottomSheetStyle.minimal.height), BottomSheetStyle.minimal), + (abs(currentHeight - BottomSheetStyle.half.height), BottomSheetStyle.half), + (abs(currentHeight - BottomSheetStyle.full.height), BottomSheetStyle.full) + ] + + return distances.min(by: { $0.0 < $1.0 })?.1 ?? .minimal + } + + var body: some View { + GeometryReader { _ in + VStack(spacing: 0) { + VStack(spacing: 8) { + RoundedRectangle(cornerRadius: 3) + .fill(Color.gray200) + .frame(width: 24.adjusted, height: 2.adjustedH) + .padding(.top, 10) + + HStack(spacing: 4) { + Text("검색 결과") + .customFont(.body2b) + Text("\(viewModel.searchPickList.count)") + .customFont(.body2b) + .foregroundColor(.gray500) + } + .padding(.bottom, 8) + } + .frame(height: 60.adjustedH) + .background(.white) + + ScrollView(showsIndicators: false) { + VStack(spacing: 0) { + LazyVStack(spacing: 0) { + ForEach(viewModel.searchPickList, id: \.placeId) { pickCard in // ✅ 검색된 장소 리스트 사용 + SearchLocationCardItem(pickCard: pickCard) + .background(Color.white) + .onTapGesture { + if currentStyle == .half { + withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) { + currentStyle = .full + isScrollEnabled = true + } + } + if let placeId = pickCard.placeId { + viewModel.fetchFocusedPlace(placeId: placeId) + } + } + } + } + Color.clear.frame(height: 90.adjusted) + } + } + .coordinateSpace(name: "scrollView") + .simultaneousGesture( + DragGesture() + .onChanged { value in + if currentStyle == .half && value.translation.height < 0 { + withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) { + currentStyle = .full + isScrollEnabled = true + } + } + } + ) + .disabled(!isScrollEnabled) + } + .frame(maxHeight: .infinity) + .background(.white) + .cornerRadius(10, corners: [.topLeft, .topRight]) + .offset(y: UIScreen.main.bounds.height - currentStyle.height + offset) + } + } +} From 3f57ab4fe5f951339f0b9e3892e57d8658fe037e Mon Sep 17 00:00:00 2001 From: hooni Date: Fri, 31 Jan 2025 03:42:29 +0900 Subject: [PATCH 06/14] =?UTF-8?q?feat:=20#172=20API=EA=B9=8C=EC=A7=80?= =?UTF-8?q?=EB=8A=94=20=EB=B6=88=EB=9F=AC=EC=98=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Network/Model/SearchResult.swift | 30 ++- .../SearchResultBottomSheet.swift | 140 ++++++----- .../Spoony-iOS/Source/Feature/Home/Home.swift | 21 +- .../Source/Feature/Home/HomeViewModel.swift | 69 +++--- .../Source/Feature/Home/NMapView.swift | 220 ++++++++++++------ .../Source/Feature/Search/SearchStore.swift | 4 +- 6 files changed, 301 insertions(+), 183 deletions(-) diff --git a/Spoony-iOS/Spoony-iOS/Network/Model/SearchResult.swift b/Spoony-iOS/Spoony-iOS/Network/Model/SearchResult.swift index 6794d585..d5fa74b7 100644 --- a/Spoony-iOS/Spoony-iOS/Network/Model/SearchResult.swift +++ b/Spoony-iOS/Spoony-iOS/Network/Model/SearchResult.swift @@ -25,7 +25,7 @@ extension PickListCardResponse { photoUrl: self.photoUrl, latitude: self.latitude, longitude: self.longitude, - categoryColorResponse: self.categoryColorResponse.toSearchCategoryColorResponse() + categoryColorResponse: self.categoryColorResponse.toSearchCategoryColorResponse() ) } } @@ -41,3 +41,31 @@ extension BottomSheetCategoryColorResponse { ) } } + +extension SearchLocationResult { + func toPickListCardResponse() -> PickListCardResponse { + return PickListCardResponse( + placeId: self.placeId ?? 0, + placeName: self.title, + placeAddress: self.address, + postTitle: self.postTitle ?? "", + photoUrl: self.photoUrl ?? "", + latitude: self.latitude ?? 0.0, + longitude: self.longitude ?? 0.0, + categoryColorResponse: self.categoryColorResponse?.toBottomSheetCategoryColorResponse() ?? + BottomSheetCategoryColorResponse(categoryId: 0, categoryName: "", iconUrl: "", iconTextColor: "", iconBackgroundColor: "") + ) + } +} + +extension SearchCategoryColorResponse { + func toBottomSheetCategoryColorResponse() -> BottomSheetCategoryColorResponse { + return BottomSheetCategoryColorResponse( + categoryId: self.categoryId, + categoryName: self.categoryName, + iconUrl: self.iconUrl, + iconTextColor: self.iconTextColor, + iconBackgroundColor: self.iconBackgroundColor + ) + } +} diff --git a/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/SearchResult/SearchResultBottomSheet.swift b/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/SearchResult/SearchResultBottomSheet.swift index 7c0f81ac..df4a9bdd 100644 --- a/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/SearchResult/SearchResultBottomSheet.swift +++ b/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/SearchResult/SearchResultBottomSheet.swift @@ -8,90 +8,88 @@ import SwiftUI struct SearchLocationBottomSheetView: View { - @ObservedObject var viewModel: HomeViewModel + @StateObject var viewModel: HomeViewModel @State private var currentStyle: BottomSheetStyle = .minimal @State private var offset: CGFloat = 0 @GestureState private var isDragging: Bool = false @State private var scrollOffset: CGFloat = 0 @State private var isScrollEnabled: Bool = false - private var snapPoints: [CGFloat] { - [ - BottomSheetStyle.minimal.height, - BottomSheetStyle.half.height, - BottomSheetStyle.full.height - ] + // snapPoints를 computed property 대신 상수로 변경 + private let snapPoints: [CGFloat] = [ + BottomSheetStyle.minimal.height, + BottomSheetStyle.half.height, + BottomSheetStyle.full.height + ] + + // 헤더 뷰를 별도로 분리 + private var headerView: some View { + VStack(spacing: 8) { + RoundedRectangle(cornerRadius: 3) + .fill(Color.gray200) + .frame(width: 24.adjusted, height: 2.adjustedH) + .padding(.top, 10) + + HStack(spacing: 4) { + Text("검색 결과") + .customFont(.body2b) + Text("\(viewModel.pickList.count)") + .customFont(.body2b) + .foregroundColor(.gray500) + } + .padding(.bottom, 8) + } + .frame(height: 60.adjustedH) + .background(.white) } - private func getClosestSnapPoint(to offset: CGFloat) -> BottomSheetStyle { - let screenHeight = UIScreen.main.bounds.height - let currentHeight = screenHeight - offset - - let distances = [ - (abs(currentHeight - BottomSheetStyle.minimal.height), BottomSheetStyle.minimal), - (abs(currentHeight - BottomSheetStyle.half.height), BottomSheetStyle.half), - (abs(currentHeight - BottomSheetStyle.full.height), BottomSheetStyle.full) - ] - - return distances.min(by: { $0.0 < $1.0 })?.1 ?? .minimal + // 컨텐츠 뷰를 별도로 분리 + private var contentView: some View { + ScrollView(showsIndicators: false) { + LazyVStack(spacing: 0) { + ForEach(viewModel.pickList, id: \.placeId) { pickCard in + SearchLocationCardItem(pickCard: pickCard.toSearchLocationResult()) // 변환 + .background(Color.white) + .onTapGesture { + handleCardTap(pickCard: pickCard) + } + } + Color.clear.frame(height: 90.adjusted) + } + } + .coordinateSpace(name: "scrollView") + .simultaneousGesture( + DragGesture().onChanged(handleDrag) + ) + .disabled(!isScrollEnabled) + } + + // 탭 핸들러를 별도 메서드로 분리 + private func handleCardTap(pickCard: PickListCardResponse) { + if currentStyle == .half { + withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) { + currentStyle = .full + isScrollEnabled = true + } + } + viewModel.fetchFocusedPlace(placeId: pickCard.placeId) + } + + // 드래그 핸들러를 별도 메서드로 분리 + private func handleDrag(value: DragGesture.Value) { + if currentStyle == .half && value.translation.height < 0 { + withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) { + currentStyle = .full + isScrollEnabled = true + } + } } var body: some View { GeometryReader { _ in VStack(spacing: 0) { - VStack(spacing: 8) { - RoundedRectangle(cornerRadius: 3) - .fill(Color.gray200) - .frame(width: 24.adjusted, height: 2.adjustedH) - .padding(.top, 10) - - HStack(spacing: 4) { - Text("검색 결과") - .customFont(.body2b) - Text("\(viewModel.searchPickList.count)") - .customFont(.body2b) - .foregroundColor(.gray500) - } - .padding(.bottom, 8) - } - .frame(height: 60.adjustedH) - .background(.white) - - ScrollView(showsIndicators: false) { - VStack(spacing: 0) { - LazyVStack(spacing: 0) { - ForEach(viewModel.searchPickList, id: \.placeId) { pickCard in // ✅ 검색된 장소 리스트 사용 - SearchLocationCardItem(pickCard: pickCard) - .background(Color.white) - .onTapGesture { - if currentStyle == .half { - withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) { - currentStyle = .full - isScrollEnabled = true - } - } - if let placeId = pickCard.placeId { - viewModel.fetchFocusedPlace(placeId: placeId) - } - } - } - } - Color.clear.frame(height: 90.adjusted) - } - } - .coordinateSpace(name: "scrollView") - .simultaneousGesture( - DragGesture() - .onChanged { value in - if currentStyle == .half && value.translation.height < 0 { - withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) { - currentStyle = .full - isScrollEnabled = true - } - } - } - ) - .disabled(!isScrollEnabled) + headerView + contentView } .frame(maxHeight: .infinity) .background(.white) diff --git a/Spoony-iOS/Spoony-iOS/Source/Feature/Home/Home.swift b/Spoony-iOS/Spoony-iOS/Source/Feature/Home/Home.swift index 093640a3..032705d1 100644 --- a/Spoony-iOS/Spoony-iOS/Source/Feature/Home/Home.swift +++ b/Spoony-iOS/Spoony-iOS/Source/Feature/Home/Home.swift @@ -34,7 +34,6 @@ struct Home: View { } } .onChange(of: viewModel.pickList) { _ in - // Reset selection when new search results arrive selectedPlace = nil if let location = viewModel.selectedLocation { print("Moving to new location: \(location)") @@ -48,7 +47,7 @@ struct Home: View { title: locationTitle, searchText: $searchText, onBackTapped: { - viewModel.fetchPickList() // Fetch original list when going back + viewModel.fetchPickList() navigationManager.currentLocation = nil } ) @@ -73,14 +72,17 @@ struct Home: View { places: viewModel.focusedPlaces, currentPage: $currentPage ) - .padding(.bottom, 12) - .transition(.move(edge: .bottom)) + } else if navigationManager.currentLocation != nil && !viewModel.searchPickList.isEmpty { + // 검색 결과가 있을 때는 SearchLocationBottomSheetView 표시 + SearchLocationBottomSheetView(viewModel: viewModel) + .onAppear { + print("✅ Showing SearchLocationBottomSheetView with \(viewModel.searchPickList.count) items") + } + } else if !viewModel.pickList.isEmpty { + // 일반 목록이 있을 때는 BottomSheetListView 표시 + BottomSheetListView(viewModel: viewModel) } else { - if !viewModel.pickList.isEmpty { - BottomSheetListView(viewModel: viewModel) - } else { - FixedBottomSheetView() - } + FixedBottomSheetView() } } } @@ -89,7 +91,6 @@ struct Home: View { isBottomSheetPresented = true do { spoonCount = try await restaurantService.fetchSpoonCount(userId: Config.userId) - // Only fetch initial list if not in search results if navigationManager.currentLocation == nil { viewModel.fetchPickList() } diff --git a/Spoony-iOS/Spoony-iOS/Source/Feature/Home/HomeViewModel.swift b/Spoony-iOS/Spoony-iOS/Source/Feature/Home/HomeViewModel.swift index 07555a32..1ec0b1a4 100644 --- a/Spoony-iOS/Spoony-iOS/Source/Feature/Home/HomeViewModel.swift +++ b/Spoony-iOS/Spoony-iOS/Source/Feature/Home/HomeViewModel.swift @@ -11,6 +11,7 @@ import Foundation final class HomeViewModel: ObservableObject { private let service: HomeServiceType @Published private(set) var pickList: [PickListCardResponse] = [] + @Published private(set) var searchPickList: [SearchLocationResult] = [] @Published var isLoading = false @Published var focusedPlaces: [CardPlace] = [] @Published var selectedLocation: (latitude: Double, longitude: Double)? @@ -24,59 +25,69 @@ final class HomeViewModel: ObservableObject { Task { isLoading = true do { + clearFocusedPlaces() let response = try await service.fetchPickList(userId: Config.userId) - self.pickList = response.zzimCardResponses - } catch { - self.error = error - } - isLoading = false - } - } - - func fetchFocusedPlace(placeId: Int) { - Task { - isLoading = true - do { - if let selectedPlace = pickList.first(where: { $0.placeId == placeId }) { - selectedLocation = (selectedPlace.latitude, selectedPlace.longitude) + await MainActor.run { + self.pickList = response.zzimCardResponses + self.searchPickList = [] // 검색 결과 초기화 } - - let response = try await service.fetchFocusedPlace(userId: Config.userId, placeId: placeId) - self.focusedPlaces = response.zzimFocusResponseList.map { $0.toCardPlace() } } catch { self.error = error } isLoading = false } } - + @MainActor func fetchLocationList(locationId: Int) async { + print("📌 fetchLocationList called with locationId:", locationId) isLoading = true do { - // Clear existing data first clearFocusedPlaces() - selectedLocation = nil - pickList = [] - + let response = try await service.fetchLocationList(userId: Config.userId, locationId: locationId) - - // Set new data - await MainActor.run { + + await MainActor.run { // ✅ 메인 스레드에서 실행 보장 self.pickList = response.zzimCardResponses - - if let firstPlace = response.zzimCardResponses.first { - self.selectedLocation = (firstPlace.latitude, firstPlace.longitude) + self.searchPickList = response.zzimCardResponses.map { $0.toSearchLocationResult() } + + if let firstResult = response.zzimCardResponses.first { + selectedLocation = (firstResult.latitude, firstResult.longitude) } } + + print("✅ Updated pickList count:", self.pickList.count) + print("✅ Updated searchPickList count:", self.searchPickList.count) } catch { self.error = error - print("Error in fetchLocationList:", error) + print("❌ Error in fetchLocationList:", error) } isLoading = false } + func fetchFocusedPlace(placeId: Int) { + Task { + isLoading = true + do { + // searchPickList에서 먼저 찾기 + if let selectedSearchPlace = searchPickList.first(where: { $0.placeId == placeId }) { + selectedLocation = (selectedSearchPlace.latitude ?? 0.0, selectedSearchPlace.longitude ?? 0.0) + } + // pickList에서 찾기 + else if let selectedPickPlace = pickList.first(where: { $0.placeId == placeId }) { + selectedLocation = (selectedPickPlace.latitude, selectedPickPlace.longitude) + } + + let response = try await service.fetchFocusedPlace(userId: Config.userId, placeId: placeId) + self.focusedPlaces = response.zzimFocusResponseList.map { $0.toCardPlace() } + } catch { + self.error = error + } + isLoading = false + } + } + func clearFocusedPlaces() { focusedPlaces = [] } diff --git a/Spoony-iOS/Spoony-iOS/Source/Feature/Home/NMapView.swift b/Spoony-iOS/Spoony-iOS/Source/Feature/Home/NMapView.swift index 611e77be..f82f1aae 100644 --- a/Spoony-iOS/Spoony-iOS/Source/Feature/Home/NMapView.swift +++ b/Spoony-iOS/Spoony-iOS/Source/Feature/Home/NMapView.swift @@ -8,6 +8,12 @@ import SwiftUI import NMapsMap + +enum MarkerData { + case search(SearchLocationResult) + case pick(PickListCardResponse) +} + struct NMapView: UIViewRepresentable { private let defaultZoomLevel: Double = 11.5 private let defaultMarker = NMFOverlayImage(name: "ic_unselected_marker") @@ -73,23 +79,23 @@ struct NMapView: UIViewRepresentable { } context.coordinator.markers.removeAll() - let newMarkers = viewModel.pickList.map { pickCard in - let marker = createMarker(for: pickCard) - marker.mapView = mapView - return marker - } - - // 처음 지도가 로드될 때만 모든 마커가 보이도록 카메라 이동 - if context.coordinator.isInitialLoad && !viewModel.pickList.isEmpty { - let bounds = NMGLatLngBounds(latLngs: viewModel.pickList.map { - NMGLatLng(lat: $0.latitude, lng: $0.longitude) - }) - let cameraUpdate = NMFCameraUpdate(fit: bounds, padding: 50) - mapView.moveCamera(cameraUpdate) - context.coordinator.isInitialLoad = false + // ✅ searchPickList가 있으면 우선적으로 사용, 없으면 pickList 사용 + let markersData: [MarkerData] = !viewModel.searchPickList.isEmpty + ? viewModel.searchPickList.map { MarkerData.search($0) } + : viewModel.pickList.map { MarkerData.pick($0) } + + let newMarkers = markersData.map { markerData in + switch markerData { + case .search(let searchResult): + return createMarker(for: searchResult) + case .pick(let pickCard): + return createMarker(for: pickCard) + } } + + newMarkers.forEach { $0.mapView = mapView } - // 마커가 선택됐을 때만 해당 위치로 카메라 이동 + // ✅ 선택된 위치로 카메라 이동 if let location = viewModel.selectedLocation { let coord = NMGLatLng(lat: location.latitude, lng: location.longitude) let cameraUpdate = NMFCameraUpdate(scrollTo: coord) @@ -97,8 +103,80 @@ struct NMapView: UIViewRepresentable { cameraUpdate.animationDuration = 0.2 mapView.moveCamera(cameraUpdate) } - + // ✅ 마커가 여러 개 있을 경우 자동으로 fit + else if context.coordinator.isInitialLoad && !markersData.isEmpty { + let bounds = NMGLatLngBounds(latLngs: markersData.map { + switch $0 { + case .search(let searchResult): + return NMGLatLng(lat: searchResult.latitude ?? 0, lng: searchResult.longitude ?? 0) + case .pick(let pickCard): + return NMGLatLng(lat: pickCard.latitude, lng: pickCard.longitude) + } + }) + let cameraUpdate = NMFCameraUpdate(fit: bounds, padding: 50) + mapView.moveCamera(cameraUpdate) + context.coordinator.isInitialLoad = false + } + context.coordinator.markers = newMarkers + +// // 선택된 위치가 있을 때 카메라 이동 +// if let location = viewModel.selectedLocation { +// let coord = NMGLatLng(lat: location.latitude, lng: location.longitude) +// let cameraUpdate = NMFCameraUpdate(scrollTo: coord) +// cameraUpdate.animation = .easeIn +// cameraUpdate.animationDuration = 0.2 +// mapView.moveCamera(cameraUpdate) +// } +// // 처음 로드될 때 모든 마커가 보이도록 +// else if context.coordinator.isInitialLoad && !markersData.isEmpty { +// let bounds = NMGLatLngBounds(latLngs: markersData.map { +// NMGLatLng(lat: $0.latitude, lng: $0.longitude) +// }) +// let cameraUpdate = NMFCameraUpdate(fit: bounds, padding: 50) +// mapView.moveCamera(cameraUpdate) +// context.coordinator.isInitialLoad = false +// } +// +// context.coordinator.markers = newMarkers + } + + private func createMarker(for searchResult: SearchLocationResult) -> NMFMarker { + let marker = NMFMarker() + marker.position = NMGLatLng(lat: searchResult.latitude ?? 0, lng: searchResult.longitude ?? 0) + marker.width = CGFloat(NMF_MARKER_SIZE_AUTO) + marker.height = CGFloat(NMF_MARKER_SIZE_AUTO) + + let isSelected = selectedPlace?.placeId == searchResult.placeId + + if isSelected { + marker.iconImage = selectedMarker + } else { + marker.iconImage = defaultMarker + } + + configureMarkerCaption(marker, with: searchResult.title, isSelected: isSelected) + + marker.touchHandler = { [weak viewModel, weak marker] (_) -> Bool in + guard let marker = marker else { return false } + + let isCurrentlySelected = (marker.iconImage == selectedMarker) + + if !isCurrentlySelected { + marker.iconImage = selectedMarker + configureMarkerCaption(marker, with: searchResult.title, isSelected: true) + viewModel?.fetchFocusedPlace(placeId: searchResult.placeId ?? 0) + } else { + resetMarker(marker) + marker.mapView = mapView + selectedPlace = nil + viewModel?.clearFocusedPlaces() + } + + return true + } + + return marker } private func configureMapView(context: Context) -> NMFMapView { @@ -133,73 +211,73 @@ struct NMapView: UIViewRepresentable { } private func createMarker(for pickCard: PickListCardResponse) -> NMFMarker { - let marker = NMFMarker() - marker.position = NMGLatLng(lat: pickCard.latitude, lng: pickCard.longitude) - marker.width = CGFloat(NMF_MARKER_SIZE_AUTO) - marker.height = CGFloat(NMF_MARKER_SIZE_AUTO) - - let isSelected = selectedPlace?.placeId == pickCard.placeId - - if isSelected { - marker.iconImage = selectedMarker - } else { - marker.iconImage = defaultMarker - } - - configureMarkerCaption(marker, with: pickCard.placeName, isSelected: isSelected) - - marker.touchHandler = { [weak viewModel, weak marker] (_) -> Bool in - guard let marker = marker else { return false } + let marker = NMFMarker() + marker.position = NMGLatLng(lat: pickCard.latitude, lng: pickCard.longitude) + marker.width = CGFloat(NMF_MARKER_SIZE_AUTO) + marker.height = CGFloat(NMF_MARKER_SIZE_AUTO) - let isCurrentlySelected = (marker.iconImage == selectedMarker) + let isSelected = selectedPlace?.placeId == pickCard.placeId - if !isCurrentlySelected { + if isSelected { marker.iconImage = selectedMarker - configureMarkerCaption(marker, with: pickCard.placeName, isSelected: true) - viewModel?.fetchFocusedPlace(placeId: pickCard.placeId) } else { - resetMarker(marker) - marker.mapView = mapView - selectedPlace = nil - viewModel?.clearFocusedPlaces() + marker.iconImage = defaultMarker } - return true + configureMarkerCaption(marker, with: pickCard.placeName, isSelected: isSelected) + + marker.touchHandler = { [weak viewModel, weak marker] (_) -> Bool in + guard let marker = marker else { return false } + + let isCurrentlySelected = (marker.iconImage == selectedMarker) + + if !isCurrentlySelected { + marker.iconImage = selectedMarker + configureMarkerCaption(marker, with: pickCard.placeName, isSelected: true) + viewModel?.fetchFocusedPlace(placeId: pickCard.placeId) + } else { + resetMarker(marker) + marker.mapView = mapView + selectedPlace = nil + viewModel?.clearFocusedPlaces() + } + + return true + } + + return marker // 마커 반환 추가 } - - return marker // 마커 반환 추가 - } } - -final class Coordinator: NSObject, NMFMapViewTouchDelegate { - @Binding var selectedPlace: CardPlace? - var markers: [NMFMarker] = [] - var isInitialLoad: Bool = true - private let defaultMarkerImage: NMFOverlayImage - private let viewModel: HomeViewModel - init(selectedPlace: Binding, - defaultMarkerImage: NMFOverlayImage, - viewModel: HomeViewModel) { - self._selectedPlace = selectedPlace - self.defaultMarkerImage = defaultMarkerImage - self.viewModel = viewModel - } - - @MainActor func mapView(_ mapView: NMFMapView, didTapMap latlng: NMGLatLng) -> Bool { - selectedPlace = nil + final class Coordinator: NSObject, NMFMapViewTouchDelegate { + @Binding var selectedPlace: CardPlace? + var markers: [NMFMarker] = [] + var isInitialLoad: Bool = true + private let defaultMarkerImage: NMFOverlayImage + private let viewModel: HomeViewModel - markers.forEach { marker in - marker.iconImage = defaultMarkerImage - marker.captionMinZoom = 10 + init(selectedPlace: Binding, + defaultMarkerImage: NMFOverlayImage, + viewModel: HomeViewModel) { + self._selectedPlace = selectedPlace + self.defaultMarkerImage = defaultMarkerImage + self.viewModel = viewModel } - if !viewModel.focusedPlaces.isEmpty { - DispatchQueue.main.async { - self.viewModel.clearFocusedPlaces() + @MainActor func mapView(_ mapView: NMFMapView, didTapMap latlng: NMGLatLng) -> Bool { + selectedPlace = nil + + markers.forEach { marker in + marker.iconImage = defaultMarkerImage + marker.captionMinZoom = 10 } + + if !viewModel.focusedPlaces.isEmpty { + DispatchQueue.main.async { + self.viewModel.clearFocusedPlaces() + } + } + + return true } - - return true } -} diff --git a/Spoony-iOS/Spoony-iOS/Source/Feature/Search/SearchStore.swift b/Spoony-iOS/Spoony-iOS/Source/Feature/Search/SearchStore.swift index 03b24de8..d7a5eb90 100644 --- a/Spoony-iOS/Spoony-iOS/Source/Feature/Search/SearchStore.swift +++ b/Spoony-iOS/Spoony-iOS/Source/Feature/Search/SearchStore.swift @@ -84,13 +84,15 @@ final class SearchStore: ObservableObject { private func handleLocationSelection(_ result: SearchResult) { Task { do { + print("🔍 Searching location list for locationId:", result.locationId) await homeViewModel.fetchLocationList(locationId: result.locationId) await MainActor.run { navigationManager.currentLocation = result.title navigationManager.pop(1) } + print("✅ Location selection completed") } catch { - print("Failed to fetch location list:", error) + print("❌ Failed to fetch location list:", error) } } } From 3e607573728cacbbcaf05c4dbf2812490766a91e Mon Sep 17 00:00:00 2001 From: hooni Date: Fri, 31 Jan 2025 04:44:39 +0900 Subject: [PATCH 07/14] =?UTF-8?q?feat:=20#172=20actor=20=EB=B6=84=EA=B8=B0?= =?UTF-8?q?=EC=8B=9C=20=EC=A7=9D=EC=88=98=EB=B2=88=EC=A7=B8=EC=97=90?= =?UTF-8?q?=EB=A7=8C=20=ED=98=B8=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SearchResultBottomSheet.swift | 184 +++++++++------ .../Resource/Tab/Model/ViewType.swift | 1 + .../Resource/Tab/NavigationManager.swift | 8 +- .../Spoony-iOS/Source/Feature/Home/Home.swift | 58 ++--- .../Source/Feature/Home/HomeViewModel.swift | 75 +++--- .../Source/Feature/Home/NMapView.swift | 220 ++++++------------ .../Source/Feature/Home/NavigationActor.swift | 44 ++++ .../Source/Feature/Search/SearchStore.swift | 60 ++--- .../Feature/Search/Views/SearchView.swift | 15 +- .../SearchLocation/SearchLocation.swift | 79 +++++++ 10 files changed, 397 insertions(+), 347 deletions(-) create mode 100644 Spoony-iOS/Spoony-iOS/Source/Feature/Home/NavigationActor.swift create mode 100644 Spoony-iOS/Spoony-iOS/Source/Feature/SearchLocation/SearchLocation.swift diff --git a/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/SearchResult/SearchResultBottomSheet.swift b/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/SearchResult/SearchResultBottomSheet.swift index df4a9bdd..753f269e 100644 --- a/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/SearchResult/SearchResultBottomSheet.swift +++ b/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/SearchResult/SearchResultBottomSheet.swift @@ -8,93 +8,139 @@ import SwiftUI struct SearchLocationBottomSheetView: View { - @StateObject var viewModel: HomeViewModel + @ObservedObject var viewModel: HomeViewModel @State private var currentStyle: BottomSheetStyle = .minimal @State private var offset: CGFloat = 0 @GestureState private var isDragging: Bool = false @State private var scrollOffset: CGFloat = 0 @State private var isScrollEnabled: Bool = false - // snapPoints를 computed property 대신 상수로 변경 - private let snapPoints: [CGFloat] = [ - BottomSheetStyle.minimal.height, - BottomSheetStyle.half.height, - BottomSheetStyle.full.height - ] - - // 헤더 뷰를 별도로 분리 - private var headerView: some View { - VStack(spacing: 8) { - RoundedRectangle(cornerRadius: 3) - .fill(Color.gray200) - .frame(width: 24.adjusted, height: 2.adjustedH) - .padding(.top, 10) - - HStack(spacing: 4) { - Text("검색 결과") - .customFont(.body2b) - Text("\(viewModel.pickList.count)") - .customFont(.body2b) - .foregroundColor(.gray500) - } - .padding(.bottom, 8) - } - .frame(height: 60.adjustedH) - .background(.white) + private var snapPoints: [CGFloat] { + [ + BottomSheetStyle.minimal.height, + BottomSheetStyle.half.height, + BottomSheetStyle.full.height + ] } - // 컨텐츠 뷰를 별도로 분리 - private var contentView: some View { - ScrollView(showsIndicators: false) { - LazyVStack(spacing: 0) { - ForEach(viewModel.pickList, id: \.placeId) { pickCard in - SearchLocationCardItem(pickCard: pickCard.toSearchLocationResult()) // 변환 - .background(Color.white) - .onTapGesture { - handleCardTap(pickCard: pickCard) - } - } - Color.clear.frame(height: 90.adjusted) - } - } - .coordinateSpace(name: "scrollView") - .simultaneousGesture( - DragGesture().onChanged(handleDrag) - ) - .disabled(!isScrollEnabled) - } - - // 탭 핸들러를 별도 메서드로 분리 - private func handleCardTap(pickCard: PickListCardResponse) { - if currentStyle == .half { - withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) { - currentStyle = .full - isScrollEnabled = true - } - } - viewModel.fetchFocusedPlace(placeId: pickCard.placeId) - } - - // 드래그 핸들러를 별도 메서드로 분리 - private func handleDrag(value: DragGesture.Value) { - if currentStyle == .half && value.translation.height < 0 { - withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) { - currentStyle = .full - isScrollEnabled = true - } - } + private func getClosestSnapPoint(to offset: CGFloat) -> BottomSheetStyle { + let screenHeight = UIScreen.main.bounds.height + let currentHeight = screenHeight - offset + + let distances = [ + (abs(currentHeight - BottomSheetStyle.minimal.height), BottomSheetStyle.minimal), + (abs(currentHeight - BottomSheetStyle.half.height), BottomSheetStyle.half), + (abs(currentHeight - BottomSheetStyle.full.height), BottomSheetStyle.full) + ] + + return distances.min(by: { $0.0 < $1.0 })?.1 ?? .minimal } var body: some View { GeometryReader { _ in VStack(spacing: 0) { - headerView - contentView + VStack(spacing: 8) { + RoundedRectangle(cornerRadius: 3) + .fill(Color.gray200) + .frame(width: 24.adjusted, height: 2.adjustedH) + .padding(.top, 10) + + HStack(spacing: 4) { + Text("검색 결과") + .customFont(.body2b) + Text("\(viewModel.pickList.count)") + .customFont(.body2b) + .foregroundColor(.gray500) + } + .padding(.bottom, 8) + } + .frame(height: 60.adjustedH) + .background(.white) + + ScrollView(showsIndicators: false) { + VStack(spacing: 0) { + LazyVStack(spacing: 0) { + ForEach(viewModel.pickList, id: \.placeId) { pickCard in + SearchLocationCardItem(pickCard: pickCard.toSearchLocationResult()) + .background(Color.white) + .onTapGesture { + if currentStyle == .half { + withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) { + currentStyle = .full + isScrollEnabled = true + } + } + viewModel.fetchFocusedPlace(placeId: pickCard.placeId) + } + } + } + Color.clear.frame(height: 90.adjusted) + } + } + .coordinateSpace(name: "scrollView") + .simultaneousGesture( + DragGesture() + .onChanged { value in + if currentStyle == .half && value.translation.height < 0 { + withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) { + currentStyle = .full + isScrollEnabled = true + } + } + } + ) + .disabled(!isScrollEnabled) } .frame(maxHeight: .infinity) .background(.white) .cornerRadius(10, corners: [.topLeft, .topRight]) .offset(y: UIScreen.main.bounds.height - currentStyle.height + offset) + .gesture( + DragGesture() + .updating($isDragging) { _, state, _ in + state = true + } + .onChanged { value in + let translation = value.translation.height + offset = translation + } + .onEnded { value in + let translation = value.translation.height + let velocity = value.predictedEndTranslation.height - translation + + if abs(velocity) > 500 { + if velocity > 0 { + switch currentStyle { + case .full: + currentStyle = .half + isScrollEnabled = false + case .half: + currentStyle = .minimal + case .minimal: break + } + } else { + switch currentStyle { + case .full: break + case .half: + currentStyle = .full + isScrollEnabled = true + case .minimal: + currentStyle = .half + } + } + } else { + let screenHeight = UIScreen.main.bounds.height + let currentOffset = screenHeight - currentStyle.height + translation + let newStyle = getClosestSnapPoint(to: currentOffset) + currentStyle = newStyle + isScrollEnabled = (newStyle == .full) + } + + withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) { + offset = 0 + } + } + ) } } } diff --git a/Spoony-iOS/Spoony-iOS/Resource/Tab/Model/ViewType.swift b/Spoony-iOS/Spoony-iOS/Resource/Tab/Model/ViewType.swift index 356672b3..bcbb15b2 100644 --- a/Spoony-iOS/Spoony-iOS/Resource/Tab/Model/ViewType.swift +++ b/Spoony-iOS/Spoony-iOS/Resource/Tab/Model/ViewType.swift @@ -13,5 +13,6 @@ enum ViewType: Hashable { case detailView(postId: Int) // 상세 화면 case explore + case searchLocationView(locationId: Int, locationTitle: String) case report(postId: Int) } diff --git a/Spoony-iOS/Spoony-iOS/Resource/Tab/NavigationManager.swift b/Spoony-iOS/Spoony-iOS/Resource/Tab/NavigationManager.swift index 72bac104..b4909d43 100644 --- a/Spoony-iOS/Spoony-iOS/Resource/Tab/NavigationManager.swift +++ b/Spoony-iOS/Spoony-iOS/Resource/Tab/NavigationManager.swift @@ -20,11 +20,7 @@ final class NavigationManager: ObservableObject { func build(_ view: ViewType) -> some View { switch view { case .searchView: - if case .map = selectedTab { - SearchView(homeViewModel: HomeViewModel(service: DefaultHomeService())) - } else { - SearchView(homeViewModel: HomeViewModel(service: DefaultHomeService())) - } + SearchView() case .locationView: Home() case .detailView(let postId): @@ -33,6 +29,8 @@ final class NavigationManager: ObservableObject { Explore() case .report(let postId): Report(postId: postId) + case .searchLocationView(locationId: let locationId, locationTitle: let locationTitle): + SearchLocation(locationId: locationId, locationTitle: locationTitle) } } diff --git a/Spoony-iOS/Spoony-iOS/Source/Feature/Home/Home.swift b/Spoony-iOS/Spoony-iOS/Source/Feature/Home/Home.swift index 032705d1..49ba92d5 100644 --- a/Spoony-iOS/Spoony-iOS/Source/Feature/Home/Home.swift +++ b/Spoony-iOS/Spoony-iOS/Source/Feature/Home/Home.swift @@ -33,36 +33,17 @@ struct Home: View { selectedPlace = newPlaces[0] } } - .onChange(of: viewModel.pickList) { _ in - selectedPlace = nil - if let location = viewModel.selectedLocation { - print("Moving to new location: \(location)") - } - } VStack(spacing: 0) { - if let locationTitle = navigationManager.currentLocation { - CustomNavigationBar( - style: .locationTitle, - title: locationTitle, - searchText: $searchText, - onBackTapped: { - viewModel.fetchPickList() - navigationManager.currentLocation = nil - } - ) - .frame(height: 56.adjusted) - } else { - CustomNavigationBar( - style: .searchContent, - searchText: $searchText, - spoonCount: spoonCount, - tappedAction: { - navigationManager.push(.searchView) - } - ) - .frame(height: 56.adjusted) - } + CustomNavigationBar( + style: .searchContent, + searchText: $searchText, + spoonCount: spoonCount, + tappedAction: { + navigationManager.push(.searchView) + } + ) + .frame(height: 56.adjusted) Spacer() } @@ -72,17 +53,14 @@ struct Home: View { places: viewModel.focusedPlaces, currentPage: $currentPage ) - } else if navigationManager.currentLocation != nil && !viewModel.searchPickList.isEmpty { - // 검색 결과가 있을 때는 SearchLocationBottomSheetView 표시 - SearchLocationBottomSheetView(viewModel: viewModel) - .onAppear { - print("✅ Showing SearchLocationBottomSheetView with \(viewModel.searchPickList.count) items") - } - } else if !viewModel.pickList.isEmpty { - // 일반 목록이 있을 때는 BottomSheetListView 표시 - BottomSheetListView(viewModel: viewModel) + .padding(.bottom, 12) + .transition(.move(edge: .bottom)) } else { - FixedBottomSheetView() + if !viewModel.pickList.isEmpty { + BottomSheetListView(viewModel: viewModel) + } else { + FixedBottomSheetView() + } } } } @@ -91,9 +69,7 @@ struct Home: View { isBottomSheetPresented = true do { spoonCount = try await restaurantService.fetchSpoonCount(userId: Config.userId) - if navigationManager.currentLocation == nil { - viewModel.fetchPickList() - } + viewModel.fetchPickList() } catch { print("Failed to fetch spoon count:", error) } diff --git a/Spoony-iOS/Spoony-iOS/Source/Feature/Home/HomeViewModel.swift b/Spoony-iOS/Spoony-iOS/Source/Feature/Home/HomeViewModel.swift index 1ec0b1a4..887d2fb6 100644 --- a/Spoony-iOS/Spoony-iOS/Source/Feature/Home/HomeViewModel.swift +++ b/Spoony-iOS/Spoony-iOS/Source/Feature/Home/HomeViewModel.swift @@ -9,9 +9,8 @@ import Foundation @MainActor final class HomeViewModel: ObservableObject { - private let service: HomeServiceType + let service: HomeServiceType @Published private(set) var pickList: [PickListCardResponse] = [] - @Published private(set) var searchPickList: [SearchLocationResult] = [] @Published var isLoading = false @Published var focusedPlaces: [CardPlace] = [] @Published var selectedLocation: (latitude: Double, longitude: Double)? @@ -25,58 +24,21 @@ final class HomeViewModel: ObservableObject { Task { isLoading = true do { - clearFocusedPlaces() let response = try await service.fetchPickList(userId: Config.userId) - await MainActor.run { - self.pickList = response.zzimCardResponses - self.searchPickList = [] // 검색 결과 초기화 - } + self.pickList = response.zzimCardResponses } catch { self.error = error } isLoading = false } } - - @MainActor - func fetchLocationList(locationId: Int) async { - print("📌 fetchLocationList called with locationId:", locationId) - isLoading = true - do { - clearFocusedPlaces() - - let response = try await service.fetchLocationList(userId: Config.userId, locationId: locationId) - - await MainActor.run { // ✅ 메인 스레드에서 실행 보장 - self.pickList = response.zzimCardResponses - self.searchPickList = response.zzimCardResponses.map { $0.toSearchLocationResult() } - - if let firstResult = response.zzimCardResponses.first { - selectedLocation = (firstResult.latitude, firstResult.longitude) - } - } - - print("✅ Updated pickList count:", self.pickList.count) - print("✅ Updated searchPickList count:", self.searchPickList.count) - } catch { - self.error = error - print("❌ Error in fetchLocationList:", error) - } - isLoading = false - } - func fetchFocusedPlace(placeId: Int) { Task { isLoading = true do { - // searchPickList에서 먼저 찾기 - if let selectedSearchPlace = searchPickList.first(where: { $0.placeId == placeId }) { - selectedLocation = (selectedSearchPlace.latitude ?? 0.0, selectedSearchPlace.longitude ?? 0.0) - } - // pickList에서 찾기 - else if let selectedPickPlace = pickList.first(where: { $0.placeId == placeId }) { - selectedLocation = (selectedPickPlace.latitude, selectedPickPlace.longitude) + if let selectedPlace = pickList.first(where: { $0.placeId == placeId }) { + selectedLocation = (selectedPlace.latitude, selectedPlace.longitude) } let response = try await service.fetchFocusedPlace(userId: Config.userId, placeId: placeId) @@ -87,7 +49,34 @@ final class HomeViewModel: ObservableObject { isLoading = false } } - + + func fetchLocationList(locationId: Int) async { + isLoading = true + do { + clearFocusedPlaces() + selectedLocation = nil + + // API 호출하여 새 데이터 받아오기 + let response = try await service.fetchLocationList( + userId: Config.userId, + locationId: locationId + ) + + // 데이터가 성공적으로 받아와진 후에만 기존 리스트 교체 + self.pickList = response.zzimCardResponses + + // 첫 번째 장소가 있다면 지도 중심점 이동 + if let firstPlace = response.zzimCardResponses.first { + self.selectedLocation = (firstPlace.latitude, firstPlace.longitude) + } + } catch { + self.error = error + print("Error in fetchLocationList:", error) + // 에러 발생 시 기존 데이터 유지 + } + isLoading = false + } + func clearFocusedPlaces() { focusedPlaces = [] } diff --git a/Spoony-iOS/Spoony-iOS/Source/Feature/Home/NMapView.swift b/Spoony-iOS/Spoony-iOS/Source/Feature/Home/NMapView.swift index f82f1aae..611e77be 100644 --- a/Spoony-iOS/Spoony-iOS/Source/Feature/Home/NMapView.swift +++ b/Spoony-iOS/Spoony-iOS/Source/Feature/Home/NMapView.swift @@ -8,12 +8,6 @@ import SwiftUI import NMapsMap - -enum MarkerData { - case search(SearchLocationResult) - case pick(PickListCardResponse) -} - struct NMapView: UIViewRepresentable { private let defaultZoomLevel: Double = 11.5 private let defaultMarker = NMFOverlayImage(name: "ic_unselected_marker") @@ -79,104 +73,32 @@ struct NMapView: UIViewRepresentable { } context.coordinator.markers.removeAll() - // ✅ searchPickList가 있으면 우선적으로 사용, 없으면 pickList 사용 - let markersData: [MarkerData] = !viewModel.searchPickList.isEmpty - ? viewModel.searchPickList.map { MarkerData.search($0) } - : viewModel.pickList.map { MarkerData.pick($0) } - - let newMarkers = markersData.map { markerData in - switch markerData { - case .search(let searchResult): - return createMarker(for: searchResult) - case .pick(let pickCard): - return createMarker(for: pickCard) - } + let newMarkers = viewModel.pickList.map { pickCard in + let marker = createMarker(for: pickCard) + marker.mapView = mapView + return marker } - - newMarkers.forEach { $0.mapView = mapView } - // ✅ 선택된 위치로 카메라 이동 - if let location = viewModel.selectedLocation { - let coord = NMGLatLng(lat: location.latitude, lng: location.longitude) - let cameraUpdate = NMFCameraUpdate(scrollTo: coord) - cameraUpdate.animation = .easeIn - cameraUpdate.animationDuration = 0.2 - mapView.moveCamera(cameraUpdate) - } - // ✅ 마커가 여러 개 있을 경우 자동으로 fit - else if context.coordinator.isInitialLoad && !markersData.isEmpty { - let bounds = NMGLatLngBounds(latLngs: markersData.map { - switch $0 { - case .search(let searchResult): - return NMGLatLng(lat: searchResult.latitude ?? 0, lng: searchResult.longitude ?? 0) - case .pick(let pickCard): - return NMGLatLng(lat: pickCard.latitude, lng: pickCard.longitude) - } + // 처음 지도가 로드될 때만 모든 마커가 보이도록 카메라 이동 + if context.coordinator.isInitialLoad && !viewModel.pickList.isEmpty { + let bounds = NMGLatLngBounds(latLngs: viewModel.pickList.map { + NMGLatLng(lat: $0.latitude, lng: $0.longitude) }) let cameraUpdate = NMFCameraUpdate(fit: bounds, padding: 50) mapView.moveCamera(cameraUpdate) context.coordinator.isInitialLoad = false } - - context.coordinator.markers = newMarkers - -// // 선택된 위치가 있을 때 카메라 이동 -// if let location = viewModel.selectedLocation { -// let coord = NMGLatLng(lat: location.latitude, lng: location.longitude) -// let cameraUpdate = NMFCameraUpdate(scrollTo: coord) -// cameraUpdate.animation = .easeIn -// cameraUpdate.animationDuration = 0.2 -// mapView.moveCamera(cameraUpdate) -// } -// // 처음 로드될 때 모든 마커가 보이도록 -// else if context.coordinator.isInitialLoad && !markersData.isEmpty { -// let bounds = NMGLatLngBounds(latLngs: markersData.map { -// NMGLatLng(lat: $0.latitude, lng: $0.longitude) -// }) -// let cameraUpdate = NMFCameraUpdate(fit: bounds, padding: 50) -// mapView.moveCamera(cameraUpdate) -// context.coordinator.isInitialLoad = false -// } -// -// context.coordinator.markers = newMarkers - } - - private func createMarker(for searchResult: SearchLocationResult) -> NMFMarker { - let marker = NMFMarker() - marker.position = NMGLatLng(lat: searchResult.latitude ?? 0, lng: searchResult.longitude ?? 0) - marker.width = CGFloat(NMF_MARKER_SIZE_AUTO) - marker.height = CGFloat(NMF_MARKER_SIZE_AUTO) - let isSelected = selectedPlace?.placeId == searchResult.placeId - - if isSelected { - marker.iconImage = selectedMarker - } else { - marker.iconImage = defaultMarker - } - - configureMarkerCaption(marker, with: searchResult.title, isSelected: isSelected) - - marker.touchHandler = { [weak viewModel, weak marker] (_) -> Bool in - guard let marker = marker else { return false } - - let isCurrentlySelected = (marker.iconImage == selectedMarker) - - if !isCurrentlySelected { - marker.iconImage = selectedMarker - configureMarkerCaption(marker, with: searchResult.title, isSelected: true) - viewModel?.fetchFocusedPlace(placeId: searchResult.placeId ?? 0) - } else { - resetMarker(marker) - marker.mapView = mapView - selectedPlace = nil - viewModel?.clearFocusedPlaces() - } - - return true + // 마커가 선택됐을 때만 해당 위치로 카메라 이동 + if let location = viewModel.selectedLocation { + let coord = NMGLatLng(lat: location.latitude, lng: location.longitude) + let cameraUpdate = NMFCameraUpdate(scrollTo: coord) + cameraUpdate.animation = .easeIn + cameraUpdate.animationDuration = 0.2 + mapView.moveCamera(cameraUpdate) } - return marker + context.coordinator.markers = newMarkers } private func configureMapView(context: Context) -> NMFMapView { @@ -211,73 +133,73 @@ struct NMapView: UIViewRepresentable { } private func createMarker(for pickCard: PickListCardResponse) -> NMFMarker { - let marker = NMFMarker() - marker.position = NMGLatLng(lat: pickCard.latitude, lng: pickCard.longitude) - marker.width = CGFloat(NMF_MARKER_SIZE_AUTO) - marker.height = CGFloat(NMF_MARKER_SIZE_AUTO) + let marker = NMFMarker() + marker.position = NMGLatLng(lat: pickCard.latitude, lng: pickCard.longitude) + marker.width = CGFloat(NMF_MARKER_SIZE_AUTO) + marker.height = CGFloat(NMF_MARKER_SIZE_AUTO) + + let isSelected = selectedPlace?.placeId == pickCard.placeId + + if isSelected { + marker.iconImage = selectedMarker + } else { + marker.iconImage = defaultMarker + } + + configureMarkerCaption(marker, with: pickCard.placeName, isSelected: isSelected) + + marker.touchHandler = { [weak viewModel, weak marker] (_) -> Bool in + guard let marker = marker else { return false } - let isSelected = selectedPlace?.placeId == pickCard.placeId + let isCurrentlySelected = (marker.iconImage == selectedMarker) - if isSelected { + if !isCurrentlySelected { marker.iconImage = selectedMarker + configureMarkerCaption(marker, with: pickCard.placeName, isSelected: true) + viewModel?.fetchFocusedPlace(placeId: pickCard.placeId) } else { - marker.iconImage = defaultMarker - } - - configureMarkerCaption(marker, with: pickCard.placeName, isSelected: isSelected) - - marker.touchHandler = { [weak viewModel, weak marker] (_) -> Bool in - guard let marker = marker else { return false } - - let isCurrentlySelected = (marker.iconImage == selectedMarker) - - if !isCurrentlySelected { - marker.iconImage = selectedMarker - configureMarkerCaption(marker, with: pickCard.placeName, isSelected: true) - viewModel?.fetchFocusedPlace(placeId: pickCard.placeId) - } else { - resetMarker(marker) - marker.mapView = mapView - selectedPlace = nil - viewModel?.clearFocusedPlaces() - } - - return true + resetMarker(marker) + marker.mapView = mapView + selectedPlace = nil + viewModel?.clearFocusedPlaces() } - return marker // 마커 반환 추가 + return true } + + return marker // 마커 반환 추가 + } } + +final class Coordinator: NSObject, NMFMapViewTouchDelegate { + @Binding var selectedPlace: CardPlace? + var markers: [NMFMarker] = [] + var isInitialLoad: Bool = true + private let defaultMarkerImage: NMFOverlayImage + private let viewModel: HomeViewModel - final class Coordinator: NSObject, NMFMapViewTouchDelegate { - @Binding var selectedPlace: CardPlace? - var markers: [NMFMarker] = [] - var isInitialLoad: Bool = true - private let defaultMarkerImage: NMFOverlayImage - private let viewModel: HomeViewModel + init(selectedPlace: Binding, + defaultMarkerImage: NMFOverlayImage, + viewModel: HomeViewModel) { + self._selectedPlace = selectedPlace + self.defaultMarkerImage = defaultMarkerImage + self.viewModel = viewModel + } + + @MainActor func mapView(_ mapView: NMFMapView, didTapMap latlng: NMGLatLng) -> Bool { + selectedPlace = nil - init(selectedPlace: Binding, - defaultMarkerImage: NMFOverlayImage, - viewModel: HomeViewModel) { - self._selectedPlace = selectedPlace - self.defaultMarkerImage = defaultMarkerImage - self.viewModel = viewModel + markers.forEach { marker in + marker.iconImage = defaultMarkerImage + marker.captionMinZoom = 10 } - @MainActor func mapView(_ mapView: NMFMapView, didTapMap latlng: NMGLatLng) -> Bool { - selectedPlace = nil - - markers.forEach { marker in - marker.iconImage = defaultMarkerImage - marker.captionMinZoom = 10 + if !viewModel.focusedPlaces.isEmpty { + DispatchQueue.main.async { + self.viewModel.clearFocusedPlaces() } - - if !viewModel.focusedPlaces.isEmpty { - DispatchQueue.main.async { - self.viewModel.clearFocusedPlaces() - } - } - - return true } + + return true } +} diff --git a/Spoony-iOS/Spoony-iOS/Source/Feature/Home/NavigationActor.swift b/Spoony-iOS/Spoony-iOS/Source/Feature/Home/NavigationActor.swift new file mode 100644 index 00000000..4e4e48f1 --- /dev/null +++ b/Spoony-iOS/Spoony-iOS/Source/Feature/Home/NavigationActor.swift @@ -0,0 +1,44 @@ +// +// NavigationActor.swift +// Spoony-iOS +// +// Created by 이지훈 on 1/30/25. +// + +import SwiftUI + +actor NavigationCoordinator { + private var isNavigating = false + + func coordinate(navigationManager: NavigationManager, result: SearchResult) async { + guard !isNavigating else { return } + isNavigating = true + + do { + // 1. Prepare data before screen transition + let targetLocationId = result.locationId + let targetTitle = result.title + + // 2. Pop current screen + await MainActor.run { + navigationManager.pop(1) + } + + // 3. Wait for UI update + try await Task.sleep(nanoseconds: 300_000_000) // 300ms delay + + // 4. Ensure data is ready before pushing a new screen + await MainActor.run { + navigationManager.push(.searchLocationView( + locationId: targetLocationId, + locationTitle: targetTitle + )) } + } catch { + print("Navigation error:", error) + } + + // 5. Reset navigation state after all tasks are complete + try? await Task.sleep(nanoseconds: 300_000_000) + isNavigating = false + } +} diff --git a/Spoony-iOS/Spoony-iOS/Source/Feature/Search/SearchStore.swift b/Spoony-iOS/Spoony-iOS/Source/Feature/Search/SearchStore.swift index d7a5eb90..eff7a45e 100644 --- a/Spoony-iOS/Spoony-iOS/Source/Feature/Search/SearchStore.swift +++ b/Spoony-iOS/Spoony-iOS/Source/Feature/Search/SearchStore.swift @@ -12,15 +12,13 @@ final class SearchStore: ObservableObject { @Published private(set) var state: SearchState = .empty @Published private(set) var model: SearchModel - private let searchService: SearchService private var navigationManager: NavigationManager - private let homeViewModel: HomeViewModel + private let navigationCoordinator = NavigationCoordinator() + private let searchService = SearchService() - init(navigationManager: NavigationManager, homeViewModel: HomeViewModel) { - self.model = SearchModel() - self.searchService = SearchService() - self.navigationManager = navigationManager - self.homeViewModel = homeViewModel + init(navigationManager: NavigationManager) { + self.navigationManager = navigationManager + self.model = SearchModel() } func dispatch(_ intent: SearchIntent) { @@ -83,16 +81,17 @@ final class SearchStore: ObservableObject { private func handleLocationSelection(_ result: SearchResult) { Task { - do { - print("🔍 Searching location list for locationId:", result.locationId) - await homeViewModel.fetchLocationList(locationId: result.locationId) - await MainActor.run { - navigationManager.currentLocation = result.title - navigationManager.pop(1) - } - print("✅ Location selection completed") - } catch { - print("❌ Failed to fetch location list:", error) + state = .loading // 로딩 상태 표시 + + // 네비게이션 수행 + await navigationCoordinator.coordinate( + navigationManager: navigationManager, + result: result + ) + + // 상태 초기화 + await MainActor.run { + state = .empty } } } @@ -115,28 +114,29 @@ final class SearchStore: ObservableObject { } await MainActor.run { - state = .success(results: results) - - if !model.recentSearches.contains(query) { - model.recentSearches.insert(query, at: 0) - if model.recentSearches.count > 6 { - model.recentSearches.removeLast() + if !results.isEmpty { + state = .success(results: results) + + if !model.recentSearches.contains(query) { + model.recentSearches.insert(query, at: 0) + if model.recentSearches.count > 6 { + model.recentSearches.removeLast() + } + saveRecentSearches() } - saveRecentSearches() + } else { + state = .error(message: "검색 결과가 없습니다") } } - } catch let error as SearchError { - print("Search error: \(error.errorDescription)") - state = .error(message: error.errorDescription) } catch { - print("Unexpected error: \(error)") - state = .error(message: "예상치 못한 오류가 발생했습니다") + print("Search error:", error) + state = .error(message: "검색 중 오류가 발생했습니다") } } } func updateNavigationManager(_ manager: NavigationManager) { - navigationManager = manager + self.navigationManager = manager } private func saveRecentSearches() { diff --git a/Spoony-iOS/Spoony-iOS/Source/Feature/Search/Views/SearchView.swift b/Spoony-iOS/Spoony-iOS/Source/Feature/Search/Views/SearchView.swift index ad3a5469..5cbed0e2 100644 --- a/Spoony-iOS/Spoony-iOS/Source/Feature/Search/Views/SearchView.swift +++ b/Spoony-iOS/Spoony-iOS/Source/Feature/Search/Views/SearchView.swift @@ -11,16 +11,10 @@ struct SearchView: View { @EnvironmentObject private var navigationManager: NavigationManager @StateObject private var store: SearchStore @FocusState private var isSearchFocused: Bool - let homeViewModel: HomeViewModel - init(homeViewModel: HomeViewModel) { - self.homeViewModel = homeViewModel + init() { let tempNavigationManager = NavigationManager() - _store = StateObject(wrappedValue: SearchStore(navigationManager: tempNavigationManager, homeViewModel: homeViewModel)) - } - - private var initStore: SearchStore { - SearchStore(navigationManager: navigationManager, homeViewModel: HomeViewModel()) + _store = StateObject(wrappedValue: SearchStore(navigationManager: tempNavigationManager)) } var body: some View { @@ -28,7 +22,7 @@ struct SearchView: View { VStack(spacing: 0) { CustomNavigationBar( style: .search(showBackButton: true), - searchText: Binding( + searchText: .init( get: { store.model.searchText }, set: { store.dispatch(.updateSearchText($0)) } ), @@ -53,13 +47,14 @@ struct SearchView: View { .navigationBarHidden(true) .onAppear { store.updateNavigationManager(navigationManager) + if store.model.isFirstAppear { isSearchFocused = true store.dispatch(.setFirstAppear(false)) } } } - + @ViewBuilder private var contentView: some View { switch store.state { diff --git a/Spoony-iOS/Spoony-iOS/Source/Feature/SearchLocation/SearchLocation.swift b/Spoony-iOS/Spoony-iOS/Source/Feature/SearchLocation/SearchLocation.swift new file mode 100644 index 00000000..e3b49d35 --- /dev/null +++ b/Spoony-iOS/Spoony-iOS/Source/Feature/SearchLocation/SearchLocation.swift @@ -0,0 +1,79 @@ +// +// SearchLocation.swift +// Spoony-iOS +// +// Created by 이지훈 on 1/30/25. +// + +import SwiftUI + +struct SearchLocation: View { + @EnvironmentObject var navigationManager: NavigationManager + @StateObject private var viewModel: HomeViewModel + @State private var selectedPlace: CardPlace? + @State private var isLoading: Bool = true + + private let locationId: Int + private let locationTitle: String + + init(locationId: Int, locationTitle: String) { + self.locationId = locationId + self.locationTitle = locationTitle + _viewModel = StateObject(wrappedValue: HomeViewModel(service: DefaultHomeService())) + } + + var body: some View { + ZStack(alignment: .bottom) { + Color.white + .edgesIgnoringSafeArea(.all) + + if isLoading { + ProgressView() + } else { + NMapView(viewModel: viewModel, selectedPlace: $selectedPlace) + .edgesIgnoringSafeArea(.all) + .onChange(of: viewModel.focusedPlaces) { newPlaces in + if !newPlaces.isEmpty { + selectedPlace = newPlaces[0] + } + } + + VStack(spacing: 0) { + CustomNavigationBar( + style: .locationTitle, + title: locationTitle, + onBackTapped: { + navigationManager.pop(2) + } + ) + .frame(height: 56.adjusted) + Spacer() + } + + Group { + if !viewModel.focusedPlaces.isEmpty { + PlaceCard( + places: viewModel.focusedPlaces, + currentPage: .constant(0) + ) + .padding(.bottom, 12) + .transition(.move(edge: .bottom)) + } else { + SearchLocationBottomSheetView(viewModel: viewModel) + } + } + } + } + .navigationBarHidden(true) + .task { + isLoading = true + do { + await viewModel.fetchLocationList(locationId: locationId) + isLoading = false + } catch { + print("Failed to fetch data:", error) + isLoading = false + } + } + } +} From 913844d4fed0699a2b4c94ae9f95e5c62b1fc82a Mon Sep 17 00:00:00 2001 From: hooni Date: Fri, 31 Jan 2025 13:47:27 +0900 Subject: [PATCH 08/14] =?UTF-8?q?feat:=20#172=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=99=84=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Resource/Tab/NavigationManager.swift | 30 +++++--- .../Source/Feature/Home/NavigationActor.swift | 69 ++++++++----------- .../Source/Feature/Search/SearchStore.swift | 23 +++---- .../Feature/Search/Views/SearchView.swift | 3 +- 4 files changed, 59 insertions(+), 66 deletions(-) diff --git a/Spoony-iOS/Spoony-iOS/Resource/Tab/NavigationManager.swift b/Spoony-iOS/Spoony-iOS/Resource/Tab/NavigationManager.swift index b4909d43..3d02d9b8 100644 --- a/Spoony-iOS/Spoony-iOS/Resource/Tab/NavigationManager.swift +++ b/Spoony-iOS/Spoony-iOS/Resource/Tab/NavigationManager.swift @@ -48,14 +48,14 @@ final class NavigationManager: ObservableObject { func pop(_ depth: Int) { switch selectedTab { case .map: - if mapPath.isEmpty || mapPath.contains(where: { - if case .locationView = $0 { return true } - return false - }) { - currentLocation = nil - } - mapPath.removeLast(depth) - + if mapPath.isEmpty { return } + if mapPath.contains(where: { + if case .locationView = $0 { return true } + return false + }) { + currentLocation = nil + } + mapPath.removeLast(min(depth, mapPath.count)) case .explore: explorePath.removeLast(depth) case .register: @@ -75,3 +75,17 @@ final class NavigationManager: ObservableObject { } } + +extension NavigationManager { + func navigateToSearchLocation(locationId: Int, locationTitle: String) { + if let lastView = mapPath.last, + case .searchView = lastView { + pop(1) + } + + push(.searchLocationView( + locationId: locationId, + locationTitle: locationTitle + )) + } +} diff --git a/Spoony-iOS/Spoony-iOS/Source/Feature/Home/NavigationActor.swift b/Spoony-iOS/Spoony-iOS/Source/Feature/Home/NavigationActor.swift index 4e4e48f1..2d02b224 100644 --- a/Spoony-iOS/Spoony-iOS/Source/Feature/Home/NavigationActor.swift +++ b/Spoony-iOS/Spoony-iOS/Source/Feature/Home/NavigationActor.swift @@ -1,44 +1,31 @@ +//// +//// NavigationActor.swift +//// Spoony-iOS +//// +//// Created by 이지훈 on 1/30/25. +//// // -// NavigationActor.swift -// Spoony-iOS +//import SwiftUI // -// Created by 이지훈 on 1/30/25. +//actor NavigationCoordinator { +// private var isNavigating = false // - -import SwiftUI - -actor NavigationCoordinator { - private var isNavigating = false - - func coordinate(navigationManager: NavigationManager, result: SearchResult) async { - guard !isNavigating else { return } - isNavigating = true - - do { - // 1. Prepare data before screen transition - let targetLocationId = result.locationId - let targetTitle = result.title - - // 2. Pop current screen - await MainActor.run { - navigationManager.pop(1) - } - - // 3. Wait for UI update - try await Task.sleep(nanoseconds: 300_000_000) // 300ms delay - - // 4. Ensure data is ready before pushing a new screen - await MainActor.run { - navigationManager.push(.searchLocationView( - locationId: targetLocationId, - locationTitle: targetTitle - )) } - } catch { - print("Navigation error:", error) - } - - // 5. Reset navigation state after all tasks are complete - try? await Task.sleep(nanoseconds: 300_000_000) - isNavigating = false - } -} +// func coordinate(navigationManager: NavigationManager, result: SearchResult) async { +// guard !isNavigating else { return } +// isNavigating = true +// +// do { +// await MainActor.run { +// navigationManager.pop(1) +// navigationManager.push(.searchLocationView( +// locationId: result.locationId, +// locationTitle: result.title +// )) +// } +// } catch { +// print("Navigation error:", error) +// } +// +// isNavigating = false +// } +//} diff --git a/Spoony-iOS/Spoony-iOS/Source/Feature/Search/SearchStore.swift b/Spoony-iOS/Spoony-iOS/Source/Feature/Search/SearchStore.swift index eff7a45e..690535f7 100644 --- a/Spoony-iOS/Spoony-iOS/Source/Feature/Search/SearchStore.swift +++ b/Spoony-iOS/Spoony-iOS/Source/Feature/Search/SearchStore.swift @@ -13,7 +13,6 @@ final class SearchStore: ObservableObject { @Published private(set) var model: SearchModel private var navigationManager: NavigationManager - private let navigationCoordinator = NavigationCoordinator() private let searchService = SearchService() init(navigationManager: NavigationManager) { @@ -80,20 +79,14 @@ final class SearchStore: ObservableObject { } private func handleLocationSelection(_ result: SearchResult) { - Task { - state = .loading // 로딩 상태 표시 - - // 네비게이션 수행 - await navigationCoordinator.coordinate( - navigationManager: navigationManager, - result: result - ) - - // 상태 초기화 - await MainActor.run { - state = .empty - } - } + state = .loading + + navigationManager.navigateToSearchLocation( + locationId: result.locationId, + locationTitle: result.title + ) + + state = .empty } private func updateSearchResults(with query: String) { diff --git a/Spoony-iOS/Spoony-iOS/Source/Feature/Search/Views/SearchView.swift b/Spoony-iOS/Spoony-iOS/Source/Feature/Search/Views/SearchView.swift index 5cbed0e2..a28735e6 100644 --- a/Spoony-iOS/Spoony-iOS/Source/Feature/Search/Views/SearchView.swift +++ b/Spoony-iOS/Spoony-iOS/Source/Feature/Search/Views/SearchView.swift @@ -13,8 +13,7 @@ struct SearchView: View { @FocusState private var isSearchFocused: Bool init() { - let tempNavigationManager = NavigationManager() - _store = StateObject(wrappedValue: SearchStore(navigationManager: tempNavigationManager)) + _store = StateObject(wrappedValue: SearchStore(navigationManager: NavigationManager())) } var body: some View { From 18d49a4a9de4df66ff16d03b652db4fbce3a02ea Mon Sep 17 00:00:00 2001 From: hooni Date: Fri, 31 Jan 2025 14:15:53 +0900 Subject: [PATCH 09/14] =?UTF-8?q?feat:=20=EA=B2=80=EC=83=89=20=EB=B0=94?= =?UTF-8?q?=ED=85=80=EC=8B=9C=ED=8A=B8=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SearchResult/SearchResultBottomSheet.swift | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/SearchResult/SearchResultBottomSheet.swift b/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/SearchResult/SearchResultBottomSheet.swift index 753f269e..9aff1a08 100644 --- a/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/SearchResult/SearchResultBottomSheet.swift +++ b/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/SearchResult/SearchResultBottomSheet.swift @@ -39,6 +39,7 @@ struct SearchLocationBottomSheetView: View { var body: some View { GeometryReader { _ in VStack(spacing: 0) { + // 핸들바 영역 VStack(spacing: 8) { RoundedRectangle(cornerRadius: 3) .fill(Color.gray200) @@ -102,7 +103,12 @@ struct SearchLocationBottomSheetView: View { } .onChanged { value in let translation = value.translation.height - offset = translation + + if currentStyle == .full && translation < 0 { + offset = 0 + } else { + offset = translation + } } .onEnded { value in let translation = value.translation.height @@ -141,6 +147,10 @@ struct SearchLocationBottomSheetView: View { } } ) + .animation(.spring(response: 0.3, dampingFraction: 0.7), value: currentStyle) + .onChange(of: currentStyle) { _, newStyle in + isScrollEnabled = (newStyle == .full) + } } } } From 15910c59c77a70f956936c2e68e304a7b4f5a42f Mon Sep 17 00:00:00 2001 From: hooni Date: Fri, 31 Jan 2025 14:18:32 +0900 Subject: [PATCH 10/14] =?UTF-8?q?feat:=20label=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BottomSheet/SearchResult/SearchResultBottomSheet.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/SearchResult/SearchResultBottomSheet.swift b/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/SearchResult/SearchResultBottomSheet.swift index 9aff1a08..3c25e24c 100644 --- a/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/SearchResult/SearchResultBottomSheet.swift +++ b/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/SearchResult/SearchResultBottomSheet.swift @@ -47,7 +47,7 @@ struct SearchLocationBottomSheetView: View { .padding(.top, 10) HStack(spacing: 4) { - Text("검색 결과") + Text("이 지역의 찐맛집") .customFont(.body2b) Text("\(viewModel.pickList.count)") .customFont(.body2b) From 1dd96e191606d7eac0c3cd7067482991b2a3e14a Mon Sep 17 00:00:00 2001 From: hooni Date: Fri, 31 Jan 2025 15:04:58 +0900 Subject: [PATCH 11/14] =?UTF-8?q?feat:=20#172=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=9E=AC=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Resource/Tab/NavigationManager.swift | 4 +-- .../Source/Feature/Home/NavigationActor.swift | 31 ------------------- .../Feature/Search/Views/SearchView.swift | 3 +- 3 files changed, 2 insertions(+), 36 deletions(-) delete mode 100644 Spoony-iOS/Spoony-iOS/Source/Feature/Home/NavigationActor.swift diff --git a/Spoony-iOS/Spoony-iOS/Resource/Tab/NavigationManager.swift b/Spoony-iOS/Spoony-iOS/Resource/Tab/NavigationManager.swift index 3d02d9b8..a9be33b2 100644 --- a/Spoony-iOS/Spoony-iOS/Resource/Tab/NavigationManager.swift +++ b/Spoony-iOS/Spoony-iOS/Resource/Tab/NavigationManager.swift @@ -74,9 +74,6 @@ final class NavigationManager: ObservableObject { } } -} - -extension NavigationManager { func navigateToSearchLocation(locationId: Int, locationTitle: String) { if let lastView = mapPath.last, case .searchView = lastView { @@ -88,4 +85,5 @@ extension NavigationManager { locationTitle: locationTitle )) } + } diff --git a/Spoony-iOS/Spoony-iOS/Source/Feature/Home/NavigationActor.swift b/Spoony-iOS/Spoony-iOS/Source/Feature/Home/NavigationActor.swift deleted file mode 100644 index 2d02b224..00000000 --- a/Spoony-iOS/Spoony-iOS/Source/Feature/Home/NavigationActor.swift +++ /dev/null @@ -1,31 +0,0 @@ -//// -//// NavigationActor.swift -//// Spoony-iOS -//// -//// Created by 이지훈 on 1/30/25. -//// -// -//import SwiftUI -// -//actor NavigationCoordinator { -// private var isNavigating = false -// -// func coordinate(navigationManager: NavigationManager, result: SearchResult) async { -// guard !isNavigating else { return } -// isNavigating = true -// -// do { -// await MainActor.run { -// navigationManager.pop(1) -// navigationManager.push(.searchLocationView( -// locationId: result.locationId, -// locationTitle: result.title -// )) -// } -// } catch { -// print("Navigation error:", error) -// } -// -// isNavigating = false -// } -//} diff --git a/Spoony-iOS/Spoony-iOS/Source/Feature/Search/Views/SearchView.swift b/Spoony-iOS/Spoony-iOS/Source/Feature/Search/Views/SearchView.swift index a28735e6..316c059e 100644 --- a/Spoony-iOS/Spoony-iOS/Source/Feature/Search/Views/SearchView.swift +++ b/Spoony-iOS/Spoony-iOS/Source/Feature/Search/Views/SearchView.swift @@ -21,7 +21,7 @@ struct SearchView: View { VStack(spacing: 0) { CustomNavigationBar( style: .search(showBackButton: true), - searchText: .init( + searchText: Binding( get: { store.model.searchText }, set: { store.dispatch(.updateSearchText($0)) } ), @@ -46,7 +46,6 @@ struct SearchView: View { .navigationBarHidden(true) .onAppear { store.updateNavigationManager(navigationManager) - if store.model.isFirstAppear { isSearchFocused = true store.dispatch(.setFirstAppear(false)) From b65889f28f5fec45cfed8099e1aefd78e0ba9e85 Mon Sep 17 00:00:00 2001 From: hooni Date: Sun, 2 Feb 2025 16:20:50 +0900 Subject: [PATCH 12/14] =?UTF-8?q?feat:=20#172=20unknown=20=ED=82=A4?= =?UTF-8?q?=EC=9B=8C=EB=93=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Resource/Components/BottomSheet/Plain/BottomSheetItem.swift | 2 +- .../BottomSheet/SearchResult/SearchLocationCardItem.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/Plain/BottomSheetItem.swift b/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/Plain/BottomSheetItem.swift index 3b9a3c3f..d5695fe4 100644 --- a/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/Plain/BottomSheetItem.swift +++ b/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/Plain/BottomSheetItem.swift @@ -84,7 +84,7 @@ struct BottomSheetListItem: View { defaultPlaceholder case .empty: defaultPlaceholder - @unknown default: + default: defaultPlaceholder } } diff --git a/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/SearchResult/SearchLocationCardItem.swift b/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/SearchResult/SearchLocationCardItem.swift index b9e7def9..42a002c3 100644 --- a/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/SearchResult/SearchLocationCardItem.swift +++ b/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/SearchResult/SearchLocationCardItem.swift @@ -85,7 +85,7 @@ struct SearchLocationCardItem: View { defaultPlaceholder case .empty: defaultPlaceholder - @unknown default: + default: defaultPlaceholder } } From 3eb5e2f26511797ad9db3cfcb09f0cf4a91e65f1 Mon Sep 17 00:00:00 2001 From: hooni Date: Sun, 2 Feb 2025 16:22:35 +0900 Subject: [PATCH 13/14] =?UTF-8?q?feat:=20#172=20coordinateSpace=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Resource/Components/BottomSheet/Plain/BottomSheetList.swift | 1 - .../BottomSheet/SearchResult/SearchResultBottomSheet.swift | 1 - .../Source/Feature/Register/View/ReviewStepView.swift | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/Plain/BottomSheetList.swift b/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/Plain/BottomSheetList.swift index f1223216..2098d947 100644 --- a/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/Plain/BottomSheetList.swift +++ b/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/Plain/BottomSheetList.swift @@ -83,7 +83,6 @@ struct BottomSheetListView: View { Color.clear.frame(height: 90.adjusted) } } - .coordinateSpace(name: "scrollView") .simultaneousGesture( DragGesture() .onChanged { value in diff --git a/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/SearchResult/SearchResultBottomSheet.swift b/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/SearchResult/SearchResultBottomSheet.swift index 3c25e24c..a52d1752 100644 --- a/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/SearchResult/SearchResultBottomSheet.swift +++ b/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/SearchResult/SearchResultBottomSheet.swift @@ -78,7 +78,6 @@ struct SearchLocationBottomSheetView: View { Color.clear.frame(height: 90.adjusted) } } - .coordinateSpace(name: "scrollView") .simultaneousGesture( DragGesture() .onChanged { value in diff --git a/Spoony-iOS/Spoony-iOS/Source/Feature/Register/View/ReviewStepView.swift b/Spoony-iOS/Spoony-iOS/Source/Feature/Register/View/ReviewStepView.swift index b3ad15e5..d685276d 100644 --- a/Spoony-iOS/Spoony-iOS/Source/Feature/Register/View/ReviewStepView.swift +++ b/Spoony-iOS/Spoony-iOS/Source/Feature/Register/View/ReviewStepView.swift @@ -42,7 +42,7 @@ struct ReviewStepView: View { .onTapGesture { hideKeyboard() } - .gesture(DragGesture(minimumDistance: 30, coordinateSpace: .local) + .gesture(DragGesture(minimumDistance: 30) .onChanged { value in if value.translation.width > 150 { store.dispatch(.movePreviousView) From eb6ff6b82ba402af5d1dca96a70904b896a1feb9 Mon Sep 17 00:00:00 2001 From: hooni Date: Thu, 6 Feb 2025 01:11:49 +0900 Subject: [PATCH 14/14] =?UTF-8?q?fix:=20#172=20=EC=BD=94=EB=A6=AC=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/BottomSheet/Plain/BottomSheetItem.swift | 7 +++---- .../BottomSheet/SearchResult/SearchLocationCardItem.swift | 7 +++---- Spoony-iOS/Spoony-iOS/Resource/Tab/NavigationManager.swift | 2 +- .../Spoony-iOS/Source/Feature/Home/HomeViewModel.swift | 2 +- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/Plain/BottomSheetItem.swift b/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/Plain/BottomSheetItem.swift index d5695fe4..78dd2385 100644 --- a/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/Plain/BottomSheetItem.swift +++ b/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/Plain/BottomSheetItem.swift @@ -28,10 +28,10 @@ struct BottomSheetListItem: View { image .resizable() .scaledToFit() - .frame(width: 16, height: 16) + .frame(width: 16.adjusted, height: 16.adjustedH) default: Color.clear - .frame(width: 16, height: 16) + .frame(width: 16.adjusted, height: 16.adjustedH) } } @@ -58,8 +58,7 @@ struct BottomSheetListItem: View { .lineLimit(1) .truncationMode(.tail) .frame(maxWidth: .infinity, alignment: .leading) - .padding(.vertical, 12) - .padding(.horizontal, 12) + .padding(12) .background(.white) .cornerRadius(8) .overlay( diff --git a/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/SearchResult/SearchLocationCardItem.swift b/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/SearchResult/SearchLocationCardItem.swift index 42a002c3..38a614a4 100644 --- a/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/SearchResult/SearchLocationCardItem.swift +++ b/Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/SearchResult/SearchLocationCardItem.swift @@ -27,10 +27,10 @@ struct SearchLocationCardItem: View { image .resizable() .scaledToFit() - .frame(width: 16, height: 16) + .frame(width: 16.adjusted, height: 16.adjustedH) default: Color.clear - .frame(width: 16, height: 16) + .frame(width: 16.adjusted, height: 16.adjustedH) } } @@ -59,8 +59,7 @@ struct SearchLocationCardItem: View { .lineLimit(1) .truncationMode(.tail) .frame(maxWidth: .infinity, alignment: .leading) - .padding(.vertical, 12) - .padding(.horizontal, 12) + .padding(12) .background(.white) .cornerRadius(8) .overlay( diff --git a/Spoony-iOS/Spoony-iOS/Resource/Tab/NavigationManager.swift b/Spoony-iOS/Spoony-iOS/Resource/Tab/NavigationManager.swift index a9be33b2..2691f301 100644 --- a/Spoony-iOS/Spoony-iOS/Resource/Tab/NavigationManager.swift +++ b/Spoony-iOS/Spoony-iOS/Resource/Tab/NavigationManager.swift @@ -16,7 +16,7 @@ final class NavigationManager: ObservableObject { @Published var popup: PopupType? - @MainActor @ViewBuilder + @ViewBuilder func build(_ view: ViewType) -> some View { switch view { case .searchView: diff --git a/Spoony-iOS/Spoony-iOS/Source/Feature/Home/HomeViewModel.swift b/Spoony-iOS/Spoony-iOS/Source/Feature/Home/HomeViewModel.swift index 887d2fb6..2c435d4a 100644 --- a/Spoony-iOS/Spoony-iOS/Source/Feature/Home/HomeViewModel.swift +++ b/Spoony-iOS/Spoony-iOS/Source/Feature/Home/HomeViewModel.swift @@ -9,7 +9,7 @@ import Foundation @MainActor final class HomeViewModel: ObservableObject { - let service: HomeServiceType + private let service: HomeServiceType @Published private(set) var pickList: [PickListCardResponse] = [] @Published var isLoading = false @Published var focusedPlaces: [CardPlace] = []