From c4a891c592cd5db2e57d417edeff913e0a7f70bd Mon Sep 17 00:00:00 2001 From: SwainYun Date: Wed, 6 May 2026 23:59:06 +0900 Subject: [PATCH 1/2] =?UTF-8?q?[Add]=20#235=20-=20=EC=B5=9C=EC=B4=88=20?= =?UTF-8?q?=EC=A7=80=EB=8F=84=20=EA=B2=80=EC=83=89=20=EC=83=81=ED=83=9C=20?= =?UTF-8?q?=ED=91=9C=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Entities/MapInitialSearchState.swift | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 Neki-iOS/Features/Map/Sources/Domain/Sources/Entities/MapInitialSearchState.swift diff --git a/Neki-iOS/Features/Map/Sources/Domain/Sources/Entities/MapInitialSearchState.swift b/Neki-iOS/Features/Map/Sources/Domain/Sources/Entities/MapInitialSearchState.swift new file mode 100644 index 00000000..0ca1f288 --- /dev/null +++ b/Neki-iOS/Features/Map/Sources/Domain/Sources/Entities/MapInitialSearchState.swift @@ -0,0 +1,17 @@ +// +// MapInitialSearchState.swift +// Neki-iOS +// +// Created by OpenAI Codex on 5/6/26. +// + +import Foundation + +/// 지도 진입 직후 첫 POI 검색이 가능한지 표현하는 상태 +public enum MapInitialSearchState: Sendable, Equatable { + case awaitingPermission + case waitingForUserLocation + case readyForDefaultLocation + case readyForUserLocation + case completed +} From 2de292971de0aa3dbb892746bd42b1b4eb67a4e4 Mon Sep 17 00:00:00 2001 From: SwainYun Date: Wed, 6 May 2026 23:59:42 +0900 Subject: [PATCH 2/2] =?UTF-8?q?[Fix]=20#235=20-=20=EC=B5=9C=EC=B4=88=20?= =?UTF-8?q?=EC=A7=80=EB=8F=84=20=EA=B2=80=EC=83=89=20=EC=83=81=ED=83=9C?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=B6=84=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Feature/MapFeature.swift | 86 ++++++++++++------- 1 file changed, 57 insertions(+), 29 deletions(-) diff --git a/Neki-iOS/Features/Map/Sources/Presentation/Sources/Feature/MapFeature.swift b/Neki-iOS/Features/Map/Sources/Presentation/Sources/Feature/MapFeature.swift index 93f550e3..1d1abbcf 100644 --- a/Neki-iOS/Features/Map/Sources/Presentation/Sources/Feature/MapFeature.swift +++ b/Neki-iOS/Features/Map/Sources/Presentation/Sources/Feature/MapFeature.swift @@ -37,8 +37,8 @@ public struct MapFeature { var isSDKAuthSuccessful: Bool = false var detent: NekiSheetDetent = SheetStage.first.detent var isSearchHereButtonVisible: Bool = false - var isFirstLoad: Bool = true var isPermissionAlertPresented: Bool = false + var initialSearchState: MapInitialSearchState = .awaitingPermission // Map State var cameraPosition: GeographicCoordinate? @@ -89,6 +89,7 @@ public struct MapFeature { case setUserTrackingMode(Bool) case didDetectMapInteraction case presentPermissionAlert + case attemptInitialSearch // Map Logic Actions case mapLoaded(GeographicBoundingBox) @@ -169,6 +170,7 @@ public struct MapFeature { state.locationAuthorizationStatus = status switch status { case .authorizedAlways, .authorizedWhenInUse: + state.initialSearchState = .waitingForUserLocation return .merge( .run { send in for await location in await mapClient.trackingLocation() { @@ -179,17 +181,14 @@ public struct MapFeature { ) case .notDetermined: + state.initialSearchState = .awaitingPermission return state.locationAuthorizationNeeded ? .send(.requestPermission) : .none case .denied, .restricted: state.isUserTrackingMode = false - - guard state.isFirstLoad, let bounds = state.currentBounds else { return .none } - state.isFirstLoad = false - return .merge( - .send(.fetchPhotoBooths(bounds: bounds)), - .send(.fetchNearbyPhotoBooths(bounds.center)) - ) + state.initialSearchState = .readyForDefaultLocation + updateCameraPosition(&state, to: Constants.defaultInitialPosition.coordinate) + return .send(.attemptInitialSearch) @unknown default: return .none @@ -214,7 +213,7 @@ public struct MapFeature { case let .mapLoaded(bounds): state.currentBounds = bounds state.cameraPosition = bounds.center - return .none + return .send(.attemptInitialSearch) case .didTapCurrentLocationButton: resetToMapMode(&state, for: .first) @@ -234,11 +233,14 @@ public struct MapFeature { case let .updateUserLocation(.success(location)): state.userLocation = location - if state.isFirstLoad || state.isUserTrackingMode { + if state.isUserTrackingMode { updateCameraPosition(&state, to: location.coordinate) - guard state.isFirstLoad else { return .none } - state.isSearchHereButtonVisible = false } + + guard state.initialSearchState == .waitingForUserLocation else { return .none } + state.isSearchHereButtonVisible = false + state.initialSearchState = .readyForUserLocation + updateCameraPosition(&state, to: location.coordinate) return .none case .updateUserLocation(.failure): @@ -263,21 +265,7 @@ public struct MapFeature { case let .cameraMotionEnded(bounds): state.currentBounds = bounds state.cameraPosition = bounds.center - - guard state.isFirstLoad else { return .none } - guard state.locationAuthorizationStatus != .notDetermined else { return .none } - - let targetCoordinate = state.isLocationAuthorized ? (state.userLocation ?? Constants.defaultInitialPosition) : Constants.defaultInitialPosition - let currentCameraLocation = CLLocation(latitude: bounds.center.latitude, longitude: bounds.center.longitude) - - guard currentCameraLocation.distance(from: targetCoordinate) <= Constants.cameraTargetDistanceThreshold else { return .none } - state.isFirstLoad = false - let nearbyTargetCoordinate = state.userGeographicCoordinate ?? bounds.center - state.lastSearchedLocation = currentCameraLocation - return .merge( - .send(.fetchPhotoBooths(bounds: bounds)), - .send(.fetchNearbyPhotoBooths(nearbyTargetCoordinate)) - ) + return .send(.attemptInitialSearch) case let .updateSearchButtonVisibility(isVisible): state.isSearchHereButtonVisible = isVisible @@ -285,7 +273,6 @@ public struct MapFeature { case .didTapSearchHereButton: guard let bounds = state.currentBounds else { return .none } - let nearbyTargetCoordinate = state.userGeographicCoordinate ?? bounds.center let currentCenterLocation = CLLocation(latitude: bounds.center.latitude, longitude: bounds.center.longitude) let isRegionChanged = checkIfRegionChanged(from: state.lastSearchedLocation, to: currentCenterLocation) let hasFilter = state.photoBoothListState.filteredBrands.isEmpty == false @@ -294,7 +281,22 @@ public struct MapFeature { return .merge( .run { _ in analytics.logEvent(event: event) }, .send(.fetchPhotoBooths(bounds: bounds)), - .send(.fetchNearbyPhotoBooths(nearbyTargetCoordinate)) + nearbyPhotoBoothsEffect(for: state) + ) + + case .attemptInitialSearch: + guard let bounds = state.currentBounds else { return .none } + guard let targetCoordinate = initialSearchTargetCoordinate(for: state) else { return .none } + + let currentCameraLocation = CLLocation(latitude: bounds.center.latitude, longitude: bounds.center.longitude) + let targetLocation = CLLocation(latitude: targetCoordinate.latitude, longitude: targetCoordinate.longitude) + + guard currentCameraLocation.distance(from: targetLocation) <= Constants.cameraTargetDistanceThreshold else { return .none } + state.initialSearchState = .completed + state.lastSearchedLocation = currentCameraLocation + return .merge( + .send(.fetchPhotoBooths(bounds: bounds)), + nearbyPhotoBoothsEffect(for: state) ) // MARK: - Data Fetching @@ -469,4 +471,30 @@ private extension MapFeature { guard let lastLocation else { return false } return currentLocation.distance(from: lastLocation) >= Constants.regionChangeDistanceThreshold } + + func initialSearchTargetCoordinate(for state: State) -> GeographicCoordinate? { + switch state.initialSearchState { + case .awaitingPermission, .waitingForUserLocation, .completed: + return nil + case .readyForDefaultLocation: + return .init( + latitude: Constants.defaultInitialPosition.coordinate.latitude, + longitude: Constants.defaultInitialPosition.coordinate.longitude + ) + case .readyForUserLocation: + return state.userGeographicCoordinate + } + } + + func nearbyPhotoBoothsEffect(for state: State) -> Effect { + guard let userGeographicCoordinate = state.userGeographicCoordinate else { + let emptyBooths = IdentifiedArrayOf() + return .merge( + .send(.photoBoothListAction(.setNearbyBooths(emptyBooths))), + .send(.photoBoothListAction(.setVisibleBooths(emptyBooths))) + ) + } + + return .send(.fetchNearbyPhotoBooths(userGeographicCoordinate)) + } }