From ca419375e3c5d248cb2f04fdf8746b9d4b0b0b40 Mon Sep 17 00:00:00 2001 From: ji-yeon224 <69784492+ji-yeon224@users.noreply.github.com> Date: Wed, 25 Feb 2026 15:38:06 +0900 Subject: [PATCH 1/7] =?UTF-8?q?refactor:=20MapViewRepresentable=20updateUI?= =?UTF-8?q?View=20=EB=A9=94=EC=84=9C=EB=93=9C=20Command=20Pattern=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 복잡한 바인딩 플래그 기반 시스템을 명령 기반 시스템으로 리팩토링 - MapAction 열거형 도입으로 지도 액션의 명시적 의도 표현 - requestBounds, updateMarkerStatus, isNeedDeleteMarker 바인딩 제거 - executeAction 메서드로 액션별 처리 로직 분리 - FlowerSpotDetailFeature에서 updateMarkerStatus delegate 패턴 적용 - updateUIView 메서드 가독성 및 유지보수성 크게 개선 --- .../Sources/FlowerSpotDetailFeature.swift | 18 +++-- .../Sources/FlowerSpotDetailFeature.swift | 2 +- .../Sources/MapFeature/MapFeature.swift | 15 ++++- .../Sources/MapFeature/MapFeature.swift | 10 +-- .../Sources/MapView/MapView.swift | 15 ++--- .../MapView/View/MapViewCoordinator.swift | 10 +-- .../MapView/View/MapViewRepresentable.swift | 65 ++++++++++++------- 7 files changed, 81 insertions(+), 54 deletions(-) diff --git a/Projects/Feature/FlowerSpotDetail/FlowerSpotDetailFeature/Sources/FlowerSpotDetailFeature.swift b/Projects/Feature/FlowerSpotDetail/FlowerSpotDetailFeature/Sources/FlowerSpotDetailFeature.swift index 4af6d54c..1a0898b5 100644 --- a/Projects/Feature/FlowerSpotDetail/FlowerSpotDetailFeature/Sources/FlowerSpotDetailFeature.swift +++ b/Projects/Feature/FlowerSpotDetail/FlowerSpotDetailFeature/Sources/FlowerSpotDetailFeature.swift @@ -192,6 +192,7 @@ extension FlowerSpotDetailFeature { if shouldUpdateMap { // 딥링크 진입: 지도 위치 이동 + 마커 표시 return .concatenate( + checkBloomStatus(status: state.flowerSpotData.bloomingStatus), .send(.prefetchImages), .send(.delegate(.showOnMap(item))), .send(.delegate(.didUpdateFlowerSpot(item))) @@ -199,6 +200,7 @@ extension FlowerSpotDetailFeature { } else { // 마커 탭/검색: 프리페치 + 부모 동기화 return .concatenate( + checkBloomStatus(status: state.flowerSpotData.bloomingStatus), .send(.prefetchImages), .send(.delegate(.didUpdateFlowerSpot(item))) ) @@ -207,12 +209,12 @@ extension FlowerSpotDetailFeature { case let .bloomingResponse(item): state.bloomingStatus = item checkLoadingComplete(&state) - return .none + return checkBloomStatus(status: state.flowerSpotData.bloomingStatus) case let .verifyTodayBlooming(item): state.isVotedBlooming = item checkLoadingComplete(&state) - return .none + return checkBloomStatus(status: state.flowerSpotData.bloomingStatus) // MARK: - Analytics @@ -263,11 +265,17 @@ extension FlowerSpotDetailFeature { state.isDetailLoading = false state.isNeedDrawPath = true - if let bloomStatus = BloomStatus(rawValue: state.flowerSpotData.bloomingStatus) { - state.updateMarkerStatus = bloomStatus - } + // Analytics 트래킹 trackDetailsStart(&state) + + } + + private func checkBloomStatus(status: String) -> Effect { + if let bloomStatus = BloomStatus(rawValue: status) { + return .send(.delegate(.updateMarkerStatus(bloomStatus))) + } + return .none } /// details_start 및 map_spot_selected 이벤트 트래킹 diff --git a/Projects/Feature/FlowerSpotDetail/FlowerSpotDetailFeatureInterface/Sources/FlowerSpotDetailFeature.swift b/Projects/Feature/FlowerSpotDetail/FlowerSpotDetailFeatureInterface/Sources/FlowerSpotDetailFeature.swift index 397bef9f..3d946426 100644 --- a/Projects/Feature/FlowerSpotDetail/FlowerSpotDetailFeatureInterface/Sources/FlowerSpotDetailFeature.swift +++ b/Projects/Feature/FlowerSpotDetail/FlowerSpotDetailFeatureInterface/Sources/FlowerSpotDetailFeature.swift @@ -58,7 +58,6 @@ public struct FlowerSpotDetailFeature { public var isShowLoginAlert: Bool = false public var isVotedBlooming: VerifyBloomingStateEntity = .init(isBlooming: false) public var isDetailLoading: Bool = false - public var updateMarkerStatus: BloomStatus? = nil public var userLocation: Coordinate? = nil // MARK: - Navigation State @@ -148,6 +147,7 @@ public struct FlowerSpotDetailFeature { case presentToLogin(id: Int) case showOnMap(FlowerSpotEntity) case didUpdateFlowerSpot(FlowerSpotEntity) + case updateMarkerStatus(BloomStatus) } public var body: some ReducerOf { diff --git a/Projects/Feature/Map/MapFeature/Sources/MapFeature/MapFeature.swift b/Projects/Feature/Map/MapFeature/Sources/MapFeature/MapFeature.swift index 4d3ba783..da25e4af 100644 --- a/Projects/Feature/Map/MapFeature/Sources/MapFeature/MapFeature.swift +++ b/Projects/Feature/Map/MapFeature/Sources/MapFeature/MapFeature.swift @@ -58,17 +58,18 @@ extension MapFeature { return .none case let .requestMapBounds(isRequest): - state.requestMapBound = isRequest + state.shouldRequestInitialBounds = true state.researchButtonEnable = false if isRequest { analyticsClient.track(MapEvent.researchClicked(currentPage: "map")) + return .send(.setMapAction(.requestBounds)) } return .none // 마커 탭 시, 디테일정보 불러오기 및 바텀시트 on case let .markerTapped(id): guard let id = id else { - state.isNeedDeleteMarker = true + state.mapAction = .deletePath state.flowerSpotDetail = nil // 바텀시트 닫기 return .none } @@ -203,7 +204,7 @@ extension MapFeature { case .dismiss: // 바텀시트 닫기: Optional State를 nil로 설정 state.flowerSpotDetail = nil - state.isNeedDeleteMarker = true + state.mapAction = .deletePath return .none case let .presentToBlooming(id, streetName, distance): @@ -223,6 +224,10 @@ extension MapFeature { state.flowerSpots[item.id] = item } return .none + + case let .updateMarkerStatus(bloomStatus): + state.mapAction = .updateMarkerStatus(bloomStatus) + return .none } case let .searchRegionList(.delegate(action)): @@ -239,6 +244,10 @@ extension MapFeature { case .flowerSpotDetail: return .none + case let .setMapAction(action): + state.mapAction = action + return .none + case .binding, .delegate, .alertAcceptTapped, .location, .searchRegionList, .mapSearch: return .none diff --git a/Projects/Feature/Map/MapFeatureInterface/Sources/MapFeature/MapFeature.swift b/Projects/Feature/Map/MapFeatureInterface/Sources/MapFeature/MapFeature.swift index 01e2adb3..ed6c91f2 100644 --- a/Projects/Feature/Map/MapFeatureInterface/Sources/MapFeature/MapFeature.swift +++ b/Projects/Feature/Map/MapFeatureInterface/Sources/MapFeature/MapFeature.swift @@ -47,15 +47,16 @@ public struct MapFeature { public var flowerSpots: [Int: FlowerSpotEntity] = [:] /// 현재 그려져있는 경로 public var selectedPathLines: [Coordinate] = [] - /// 지도에 마커 및 경로 비활성화 트리거 - public var isNeedDeleteMarker: Bool = false /// 지도에 마커 및 경로 그리기 트리거 public var isNeedDrawMarker: Bool = false - /// 지도 범위 요청 트리거 - public var requestMapBound: Bool = false + /// 초기 지도 범위 요청 트리거 (초기 진입용) + public var shouldRequestInitialBounds: Bool = false /// 현위치 재검색 버튼 활성화 여부 public var researchButtonEnable: Bool = false + /// 지도 액션 명령 + public var mapAction: MapAction? = nil + public var toastMessage: String? = nil public var toastLabel: String? = nil @@ -91,6 +92,7 @@ public struct MapFeature { case viewDidAppear case requestMapBounds(Bool) + case setMapAction(MapAction?) case markerTapped(id: Int?) case fetchPathLines(Int) case fetchDetailInfo(Int) diff --git a/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/MapView.swift b/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/MapView.swift index 0edc1629..fa428f5a 100644 --- a/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/MapView.swift +++ b/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/MapView.swift @@ -91,21 +91,18 @@ extension MapView { userLocation: $store.location.point, flowerPositions: $store.flowerSpots, newPath: $store.selectedPathLines, - requestBounds: $store.requestMapBound, isCameraMove: $store.researchButtonEnable, focusData: $store.mapSearch.searchResult, - isNeedDeleteMarker: $store.isNeedDeleteMarker, isNeedDrawMarker: $store.isNeedDrawMarker, - updateMarkerStatus: Binding( - get: { store.flowerSpotDetail?.updateMarkerStatus }, - set: { _ in } + hasBottomSheet: $store.mapSearch.isShowRegionList, + mapAction: Binding( + get: { store.mapAction }, + set: { store.send(.setMapAction($0)) } ), - hasBottomSheet: $store.mapSearch.isShowRegionList + shouldRequestInitialBounds: $store.shouldRequestInitialBounds ) .onReceiveMapBounds { - if store.requestMapBound { - store.send(.location(.fetchFlowers($0))) - } + store.send(.location(.fetchFlowers($0))) } .onMarkerTapped { id in store.send(.markerTapped(id: id)) diff --git a/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/View/MapViewCoordinator.swift b/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/View/MapViewCoordinator.swift index 1ffc6996..d020212d 100644 --- a/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/View/MapViewCoordinator.swift +++ b/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/View/MapViewCoordinator.swift @@ -56,7 +56,7 @@ extension MapViewRepresentable { func mapView(_ mapView: NMFMapView, didTapMap latlng: NMGLatLng, point: CGPoint) { if let onMarkerTapped = parent.onMarkerTapped { onMarkerTapped(nil) - parent.isNeedDeleteMarker = true + parent.mapAction = .deletePath } if let _ = focusMarker { deleteSearchResult() @@ -86,10 +86,8 @@ extension MapViewRepresentable { func mapViewCameraIdle(_ mapView: NMFMapView) { // 앱 처음 진입 시 카메라 이동 완료 후 지도 범위 값 가져오도록 처리 - if isInitialBounds, parent.requestBounds { - parent.currentVisibleBounds(on: mapView) - parent.requestBounds = false - isInitialBounds = false + if isInitialBounds, parent.shouldRequestInitialBounds { + parent.mapAction = .requestInitialBounds } } @@ -140,7 +138,6 @@ extension MapViewRepresentable { } deletePath() deleteMarker() - parent.isNeedDeleteMarker = false selectedPin = data activeMarker = marker @@ -162,7 +159,6 @@ extension MapViewRepresentable { startMarker.iconImage = NMFOverlayImage(image: state.circleImage) endMarker.iconImage = NMFOverlayImage(image: state.circleImage) } - parent.updateMarkerStatus = nil } } diff --git a/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/View/MapViewRepresentable.swift b/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/View/MapViewRepresentable.swift index ac737916..ced77162 100644 --- a/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/View/MapViewRepresentable.swift +++ b/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/View/MapViewRepresentable.swift @@ -13,6 +13,13 @@ import FlowerSpotClient import BloomingClient import Shared +public enum MapAction: Equatable { + case requestBounds + case requestInitialBounds + case updateMarkerStatus(BloomStatus) + case deletePath +} + struct MapViewRepresentable: UIViewRepresentable { /// 사용자의 현재 위치 정보 /// @@ -25,27 +32,24 @@ struct MapViewRepresentable: UIViewRepresentable { /// 마커 탭 시 경로를 보여주기 위한 프로퍼티 @Binding var newPath: [Coordinate] - /// 지도 범위 요청 프로퍼티 - @Binding var requestBounds: Bool - /// 지도를 움직일 경우 현 위치 재검색 버튼 활성화 하기 위한 트리거 @Binding var isCameraMove: Bool /// 지도에 특정 위치를 표시하기 위한 프로퍼티 @Binding var focusData: FlowerSpotEntity? - /// 마커 삭제 트리거 - @Binding var isNeedDeleteMarker: Bool - /// 마커 그리기 트리거 @Binding var isNeedDrawMarker: Bool - /// 활성화 된 마커 상태를 업데이트 - @Binding var updateMarkerStatus: BloomStatus? - /// 바텀시트 표시 여부 (카메라 중앙 위치 조정용) @Binding var hasBottomSheet: Bool + /// 지도 액션 명령 + @Binding var mapAction: MapAction? + + /// 초기 bounds 요청 여부 (현재 위치 이동 완료 후 자동 실행용) + @Binding var shouldRequestInitialBounds: Bool + /// 마커 탭 시 id값을 전달하기 위한 클로저 var onMarkerTapped: ((Int?) -> Void)? = nil /// 지도 범위 좌표 값을 전달하기 위한 클로저 @@ -103,20 +107,9 @@ struct MapViewRepresentable: UIViewRepresentable { if let data = context.coordinator.selectedPin { if isNeedDrawMarker, newPath != context.coordinator.drawPathPoints { drawPathLine(uiView, data: data, for: newPath, context: context) - - } else if isNeedDeleteMarker { // 그려져있는 마커 및 경로 삭제 - context.coordinator.deletePath() - context.coordinator.deleteMarker() } } - // 현 위치 재검색 액션 - if requestBounds, !context.coordinator.isInitialBounds { - currentVisibleBounds(on: uiView.mapView) - deleteDrawMarker(context: context) - requestBounds = false - } - // 특정 위치에 나타날 데이터가 있을 경우 if let focusData = focusData, context.coordinator.focusData != focusData { drawPathLine(uiView, data: focusData, for: focusData.path, context: context) @@ -125,8 +118,10 @@ struct MapViewRepresentable: UIViewRepresentable { context.coordinator.deleteSearchResult() } - if let state = updateMarkerStatus { - context.coordinator.updateMarker(state: state) + // 액션 기반 명령 처리 + if let action = mapAction { + executeAction(action, on: uiView, context: context) + mapAction = nil } } @@ -134,6 +129,29 @@ struct MapViewRepresentable: UIViewRepresentable { return Coordinator(self) } + /// 액션 기반 명령 실행 + private func executeAction(_ action: MapAction, on uiView: NMFNaverMapView, context: Context) { + switch action { + case .requestBounds: + if !context.coordinator.isInitialBounds { + currentVisibleBounds(on: uiView.mapView) + deleteDrawMarker(context: context) + } + + case .requestInitialBounds: + currentVisibleBounds(on: uiView.mapView) + context.coordinator.isInitialBounds = false + shouldRequestInitialBounds = false + + case .updateMarkerStatus(let state): + context.coordinator.updateMarker(state: state) + + case .deletePath: + context.coordinator.deletePath() + context.coordinator.deleteMarker() + } + } + } // MARK: - Action @@ -147,7 +165,6 @@ extension MapViewRepresentable { if let marker = context.coordinator.focusMarker { marker.mapView = nil } - isNeedDeleteMarker = false context.coordinator.focusData = result let state = BloomStatus(rawValue: result.bloomingStatus) @@ -363,8 +380,6 @@ extension MapViewRepresentable { // 모든 경로 포인트가 보이도록 카메라 조정 fitCameraToPath(view, path: newPath) - - isNeedDrawMarker = false } /// 지도 위에 비활성화 마커를 표시하기 위한 메서드 From daf721ac649a88e70a1074a759d16e679672af61 Mon Sep 17 00:00:00 2001 From: ji-yeon224 <69784492+ji-yeon224@users.noreply.github.com> Date: Wed, 25 Feb 2026 15:54:36 +0900 Subject: [PATCH 2/7] =?UTF-8?q?refactor:=20userLocation=20=EB=B0=94?= =?UTF-8?q?=EC=9D=B8=EB=94=A9=EC=9D=84=20MapAction=20=EA=B8=B0=EB=B0=98?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MapAction에 moveToUserLocation(Coordinate) 액션 추가 - userLocation @Binding 제거하여 바인딩 복잡성 감소 - LocationFeature에 moveToLocation delegate 패턴 도입 - 사용자 위치 이동 로직을 Command Pattern으로 통일 - updateUIView에서 userLocation 관련 조건부 로직 제거 - TCA delegate를 통한 깔끔한 위치 이동 플로우 구현 --- .../Sources/MapFeature/MapFeature.swift | 4 ++++ .../MapFeature/SubFeature/LocationFeature.swift | 2 +- .../MapFeature/SubFeature/LocationFeature.swift | 2 ++ .../Sources/MapView/MapView.swift | 1 - .../MapView/View/MapViewRepresentable.swift | 16 +++++----------- 5 files changed, 12 insertions(+), 13 deletions(-) diff --git a/Projects/Feature/Map/MapFeature/Sources/MapFeature/MapFeature.swift b/Projects/Feature/Map/MapFeature/Sources/MapFeature/MapFeature.swift index da25e4af..7c9bb280 100644 --- a/Projects/Feature/Map/MapFeature/Sources/MapFeature/MapFeature.swift +++ b/Projects/Feature/Map/MapFeature/Sources/MapFeature/MapFeature.swift @@ -190,6 +190,10 @@ extension MapFeature { case let .presentAlert(type): return .send(.presentAlert(type: type)) + + case let .moveToLocation(location): + state.mapAction = .moveToUserLocation(location) + return .none } // MARK: - Delegate diff --git a/Projects/Feature/Map/MapFeature/Sources/MapFeature/SubFeature/LocationFeature.swift b/Projects/Feature/Map/MapFeature/Sources/MapFeature/SubFeature/LocationFeature.swift index 32c80577..1af00997 100644 --- a/Projects/Feature/Map/MapFeature/Sources/MapFeature/SubFeature/LocationFeature.swift +++ b/Projects/Feature/Map/MapFeature/Sources/MapFeature/SubFeature/LocationFeature.swift @@ -37,7 +37,7 @@ extension LocationFeature { case let .moveLocation(point): state.point = point - return .none + return .send(.delegate(.moveToLocation(point))) case let .currentButtonTapped(isTapped): state.isCurrentButtonTap = isTapped diff --git a/Projects/Feature/Map/MapFeatureInterface/Sources/MapFeature/SubFeature/LocationFeature.swift b/Projects/Feature/Map/MapFeatureInterface/Sources/MapFeature/SubFeature/LocationFeature.swift index 6fb57a3d..2748d34c 100644 --- a/Projects/Feature/Map/MapFeatureInterface/Sources/MapFeature/SubFeature/LocationFeature.swift +++ b/Projects/Feature/Map/MapFeatureInterface/Sources/MapFeature/SubFeature/LocationFeature.swift @@ -56,6 +56,8 @@ public struct LocationFeature { case storeFlowerData([FlowerSpotEntity]) /// 상위로 전달하여 하위 state에 동기화 case storeUserLocation(Coordinate?) + /// 지도 위치 이동 요청 + case moveToLocation(Coordinate) case showToastView(message: String?, buttonLabel: String?) case presentAlert(type: AlertType) diff --git a/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/MapView.swift b/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/MapView.swift index fa428f5a..ae0a9b8e 100644 --- a/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/MapView.swift +++ b/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/MapView.swift @@ -88,7 +88,6 @@ extension MapView { @ViewBuilder private var mapView: some View { MapViewRepresentable( - userLocation: $store.location.point, flowerPositions: $store.flowerSpots, newPath: $store.selectedPathLines, isCameraMove: $store.researchButtonEnable, diff --git a/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/View/MapViewRepresentable.swift b/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/View/MapViewRepresentable.swift index ced77162..e657e420 100644 --- a/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/View/MapViewRepresentable.swift +++ b/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/View/MapViewRepresentable.swift @@ -18,14 +18,10 @@ public enum MapAction: Equatable { case requestInitialBounds case updateMarkerStatus(BloomStatus) case deletePath + case moveToUserLocation(Coordinate) } struct MapViewRepresentable: UIViewRepresentable { - /// 사용자의 현재 위치 정보 - /// - /// - 초기 및 현위치 버튼 탭 시에만 값이 채워져 있음 - /// - 현 위치 이동 플래그 기능과 유사하게 동작 - @Binding var userLocation: Coordinate? /// 지도에 보여줄 데이터 @Binding var flowerPositions: [Int: FlowerSpotEntity] @@ -82,10 +78,6 @@ struct MapViewRepresentable: UIViewRepresentable { } func updateUIView(_ uiView: NMFNaverMapView, context: Context) { - if let userLocation = userLocation { - moveUserLocation(uiView, to: userLocation, context: context) - } - // 마커 데이터가 변경되었을 때 마커 업데이트 if context.coordinator.currentFlowerPositions != flowerPositions { // 마커 ID 세트가 변경된 경우에만 카메라 이동 (데이터만 갱신된 경우 스킵) @@ -131,6 +123,7 @@ struct MapViewRepresentable: UIViewRepresentable { /// 액션 기반 명령 실행 private func executeAction(_ action: MapAction, on uiView: NMFNaverMapView, context: Context) { + print(action) switch action { case .requestBounds: if !context.coordinator.isInitialBounds { @@ -149,6 +142,9 @@ struct MapViewRepresentable: UIViewRepresentable { case .deletePath: context.coordinator.deletePath() context.coordinator.deleteMarker() + + case .moveToUserLocation(let location): + moveUserLocation(uiView, to: location, context: context) } } @@ -201,9 +197,7 @@ extension MapViewRepresentable { view.mapView.positionMode = .normal moveCamera(view, to: userLocation) context.coordinator.lastCameraPoint = userLocation - } - self.userLocation = nil } /// 카메라 이동 메서드 From ab8f0b764641972d94eeb145e9175f593be0a005 Mon Sep 17 00:00:00 2001 From: ji-yeon224 <69784492+ji-yeon224@users.noreply.github.com> Date: Wed, 25 Feb 2026 16:08:46 +0900 Subject: [PATCH 3/7] =?UTF-8?q?refactor:=20=EB=A7=88=EC=BB=A4=20=ED=83=AD?= =?UTF-8?q?=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=EC=99=80=20=EA=B2=BD=EB=A1=9C=20?= =?UTF-8?q?=EA=B7=B8=EB=A6=AC=EA=B8=B0=EB=A5=BC=20MapAction.drawPath?= =?UTF-8?q?=EB=A1=9C=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MapAction에 drawPath(FlowerSpotEntity, [Coordinate]) 액션 추가 - isNeedDrawMarker, newPath 바인딩 제거로 복잡성 감소 - updateUIView에서 마커 탭 관련 조건부 로직 제거 - fetchPathLines에서 mapAction을 통한 경로 그리기 명령 통일 - 마커 탭부터 경로 표시까지 일관된 Command Pattern 적용 - MapViewRepresentable 생성자 파라미터 대폭 단순화 --- .../Sources/MapFeature/MapFeature.swift | 2 +- .../Sources/MapFeature/MapFeature.swift | 2 -- .../Sources/MapView/MapView.swift | 2 -- .../MapView/View/MapViewRepresentable.swift | 18 ++++-------------- 4 files changed, 5 insertions(+), 19 deletions(-) diff --git a/Projects/Feature/Map/MapFeature/Sources/MapFeature/MapFeature.swift b/Projects/Feature/Map/MapFeature/Sources/MapFeature/MapFeature.swift index 7c9bb280..53ba8602 100644 --- a/Projects/Feature/Map/MapFeature/Sources/MapFeature/MapFeature.swift +++ b/Projects/Feature/Map/MapFeature/Sources/MapFeature/MapFeature.swift @@ -86,7 +86,7 @@ extension MapFeature { case let .fetchPathLines(id): if let data = state.flowerSpots[id] { state.selectedPathLines = data.path - state.isNeedDrawMarker = true + state.mapAction = .drawPath(data, data.path) } else { state.selectedPathLines = [] } diff --git a/Projects/Feature/Map/MapFeatureInterface/Sources/MapFeature/MapFeature.swift b/Projects/Feature/Map/MapFeatureInterface/Sources/MapFeature/MapFeature.swift index ed6c91f2..ffda9663 100644 --- a/Projects/Feature/Map/MapFeatureInterface/Sources/MapFeature/MapFeature.swift +++ b/Projects/Feature/Map/MapFeatureInterface/Sources/MapFeature/MapFeature.swift @@ -47,8 +47,6 @@ public struct MapFeature { public var flowerSpots: [Int: FlowerSpotEntity] = [:] /// 현재 그려져있는 경로 public var selectedPathLines: [Coordinate] = [] - /// 지도에 마커 및 경로 그리기 트리거 - public var isNeedDrawMarker: Bool = false /// 초기 지도 범위 요청 트리거 (초기 진입용) public var shouldRequestInitialBounds: Bool = false /// 현위치 재검색 버튼 활성화 여부 diff --git a/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/MapView.swift b/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/MapView.swift index ae0a9b8e..f2157bec 100644 --- a/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/MapView.swift +++ b/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/MapView.swift @@ -89,10 +89,8 @@ extension MapView { private var mapView: some View { MapViewRepresentable( flowerPositions: $store.flowerSpots, - newPath: $store.selectedPathLines, isCameraMove: $store.researchButtonEnable, focusData: $store.mapSearch.searchResult, - isNeedDrawMarker: $store.isNeedDrawMarker, hasBottomSheet: $store.mapSearch.isShowRegionList, mapAction: Binding( get: { store.mapAction }, diff --git a/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/View/MapViewRepresentable.swift b/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/View/MapViewRepresentable.swift index e657e420..f9eb2b1d 100644 --- a/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/View/MapViewRepresentable.swift +++ b/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/View/MapViewRepresentable.swift @@ -19,24 +19,19 @@ public enum MapAction: Equatable { case updateMarkerStatus(BloomStatus) case deletePath case moveToUserLocation(Coordinate) + case drawPath(FlowerSpotEntity, [Coordinate]) } struct MapViewRepresentable: UIViewRepresentable { /// 지도에 보여줄 데이터 @Binding var flowerPositions: [Int: FlowerSpotEntity] - /// 마커 탭 시 경로를 보여주기 위한 프로퍼티 - @Binding var newPath: [Coordinate] - /// 지도를 움직일 경우 현 위치 재검색 버튼 활성화 하기 위한 트리거 @Binding var isCameraMove: Bool /// 지도에 특정 위치를 표시하기 위한 프로퍼티 @Binding var focusData: FlowerSpotEntity? - /// 마커 그리기 트리거 - @Binding var isNeedDrawMarker: Bool - /// 바텀시트 표시 여부 (카메라 중앙 위치 조정용) @Binding var hasBottomSheet: Bool @@ -95,13 +90,6 @@ struct MapViewRepresentable: UIViewRepresentable { } } - // 마커 탭 이벤트 시 - if let data = context.coordinator.selectedPin { - if isNeedDrawMarker, newPath != context.coordinator.drawPathPoints { - drawPathLine(uiView, data: data, for: newPath, context: context) - } - } - // 특정 위치에 나타날 데이터가 있을 경우 if let focusData = focusData, context.coordinator.focusData != focusData { drawPathLine(uiView, data: focusData, for: focusData.path, context: context) @@ -123,7 +111,6 @@ struct MapViewRepresentable: UIViewRepresentable { /// 액션 기반 명령 실행 private func executeAction(_ action: MapAction, on uiView: NMFNaverMapView, context: Context) { - print(action) switch action { case .requestBounds: if !context.coordinator.isInitialBounds { @@ -145,6 +132,9 @@ struct MapViewRepresentable: UIViewRepresentable { case .moveToUserLocation(let location): moveUserLocation(uiView, to: location, context: context) + + case .drawPath(let data, let pathCoordinates): + drawPathLine(uiView, data: data, for: pathCoordinates, context: context) } } From 4c3be3f2f131f93be591950b813f99c1ba33061a Mon Sep 17 00:00:00 2001 From: ji-yeon224 <69784492+ji-yeon224@users.noreply.github.com> Date: Thu, 26 Feb 2026 15:56:49 +0900 Subject: [PATCH 4/7] =?UTF-8?q?refactor:=20MapAction=EC=9D=84=20=ED=81=90?= =?UTF-8?q?=20=EC=8B=9C=EC=8A=A4=ED=85=9C=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=ED=95=98=EC=97=AC=20=EC=97=B0=EC=86=8D=20=EC=95=A1?= =?UTF-8?q?=EC=85=98=20=EC=B2=98=EB=A6=AC=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 단일 mapAction을 mapActions 배열 큐로 변경 - 연속으로 전달되는 액션들이 모두 순서대로 실행되도록 개선 - setMapAction을 addMapAction/clearMapActions으로 분리 - focusData 바인딩을 showFocus/clearFocus 액션으로 대체 - searchRegionList의 showFlowerSpotDetail을 changeActiveMarker 액션으로 처리 - 액션 덮어쓰기 문제 해결로 마커 활성화 및 경로 그리기 동작 안정화 --- .../Sources/FlowerSpotDetailFeature.swift | 1 + .../Sources/MapFeature/MapFeature.swift | 31 +++++++------- .../Sources/MapFeature/MapFeature.swift | 7 ++-- .../Sources/MapView/MapView.swift | 6 +-- .../MapView/View/MapViewCoordinator.swift | 7 ++-- .../MapView/View/MapViewRepresentable.swift | 40 +++++++++++-------- 6 files changed, 50 insertions(+), 42 deletions(-) diff --git a/Projects/Feature/FlowerSpotDetail/FlowerSpotDetailFeature/Sources/FlowerSpotDetailFeature.swift b/Projects/Feature/FlowerSpotDetail/FlowerSpotDetailFeature/Sources/FlowerSpotDetailFeature.swift index 1a0898b5..f841014b 100644 --- a/Projects/Feature/FlowerSpotDetail/FlowerSpotDetailFeature/Sources/FlowerSpotDetailFeature.swift +++ b/Projects/Feature/FlowerSpotDetail/FlowerSpotDetailFeature/Sources/FlowerSpotDetailFeature.swift @@ -179,6 +179,7 @@ extension FlowerSpotDetailFeature { return fetchDetailInfo(id: id) case let .detailResponse(item, shouldUpdateMap): + let prevData = state.flowerSpotData state.flowerSpotData = item state.spotId = item.id // distance 계산 diff --git a/Projects/Feature/Map/MapFeature/Sources/MapFeature/MapFeature.swift b/Projects/Feature/Map/MapFeature/Sources/MapFeature/MapFeature.swift index 53ba8602..102639f8 100644 --- a/Projects/Feature/Map/MapFeature/Sources/MapFeature/MapFeature.swift +++ b/Projects/Feature/Map/MapFeature/Sources/MapFeature/MapFeature.swift @@ -62,14 +62,14 @@ extension MapFeature { state.researchButtonEnable = false if isRequest { analyticsClient.track(MapEvent.researchClicked(currentPage: "map")) - return .send(.setMapAction(.requestBounds)) + return .send(.addMapAction(.requestBounds)) } return .none // 마커 탭 시, 디테일정보 불러오기 및 바텀시트 on case let .markerTapped(id): guard let id = id else { - state.mapAction = .deletePath + state.mapActions.append(.deletePath) state.flowerSpotDetail = nil // 바텀시트 닫기 return .none } @@ -86,7 +86,7 @@ extension MapFeature { case let .fetchPathLines(id): if let data = state.flowerSpots[id] { state.selectedPathLines = data.path - state.mapAction = .drawPath(data, data.path) + state.mapActions.append(.drawPath(data, data.path)) } else { state.selectedPathLines = [] } @@ -192,7 +192,7 @@ extension MapFeature { return .send(.presentAlert(type: type)) case let .moveToLocation(location): - state.mapAction = .moveToUserLocation(location) + state.mapActions.append(.moveToUserLocation(location)) return .none } @@ -208,7 +208,7 @@ extension MapFeature { case .dismiss: // 바텀시트 닫기: Optional State를 nil로 설정 state.flowerSpotDetail = nil - state.mapAction = .deletePath + state.mapActions.append(.deletePath) return .none case let .presentToBlooming(id, streetName, distance): @@ -230,26 +230,27 @@ extension MapFeature { return .none case let .updateMarkerStatus(bloomStatus): - state.mapAction = .updateMarkerStatus(bloomStatus) - return .none + return .send(.addMapAction(.updateMarkerStatus(bloomStatus))) } case let .searchRegionList(.delegate(action)): switch action { case let .showFlowerSpotDetail(data): return .concatenate( - .send(.mapSearch(.showRegionList(data: nil))), - .send(.mapSearch(.setNavigationFromRegionList)), - .send(.fetchDetailInfo(data.id)), - .send(.location(.moveLocation(data.pinPoint))) + .send(.addMapAction(.changeActiveMarker(data))), + .send(.markerTapped(id: data.id)) ) } case .flowerSpotDetail: return .none - case let .setMapAction(action): - state.mapAction = action + case let .addMapAction(action): + state.mapActions.append(action) + return .none + + case .clearMapActions: + state.mapActions.removeAll() return .none case .binding, .delegate, .alertAcceptTapped, .location, .searchRegionList, .mapSearch: @@ -271,8 +272,10 @@ extension MapFeature.Core { if let result = result { await send(.mapSearch(.setSearchBarText(result.streetName))) await send(.location(.moveLocation(result.pinPoint))) - await send(.fetchPathLines(result.id)) + await send(.addMapAction(.showFocus(result))) await send(.flowerSpotDetail(.requestDetailInfo(result.id))) + } else { + await send(.addMapAction(.clearFocus)) } } } diff --git a/Projects/Feature/Map/MapFeatureInterface/Sources/MapFeature/MapFeature.swift b/Projects/Feature/Map/MapFeatureInterface/Sources/MapFeature/MapFeature.swift index ffda9663..dd88e490 100644 --- a/Projects/Feature/Map/MapFeatureInterface/Sources/MapFeature/MapFeature.swift +++ b/Projects/Feature/Map/MapFeatureInterface/Sources/MapFeature/MapFeature.swift @@ -52,8 +52,8 @@ public struct MapFeature { /// 현위치 재검색 버튼 활성화 여부 public var researchButtonEnable: Bool = false - /// 지도 액션 명령 - public var mapAction: MapAction? = nil + /// 지도 액션 명령 큐 + public var mapActions: [MapAction] = [] public var toastMessage: String? = nil @@ -90,7 +90,8 @@ public struct MapFeature { case viewDidAppear case requestMapBounds(Bool) - case setMapAction(MapAction?) + case addMapAction(MapAction) + case clearMapActions case markerTapped(id: Int?) case fetchPathLines(Int) case fetchDetailInfo(Int) diff --git a/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/MapView.swift b/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/MapView.swift index f2157bec..88601e25 100644 --- a/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/MapView.swift +++ b/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/MapView.swift @@ -90,12 +90,8 @@ extension MapView { MapViewRepresentable( flowerPositions: $store.flowerSpots, isCameraMove: $store.researchButtonEnable, - focusData: $store.mapSearch.searchResult, hasBottomSheet: $store.mapSearch.isShowRegionList, - mapAction: Binding( - get: { store.mapAction }, - set: { store.send(.setMapAction($0)) } - ), + mapActions: $store.mapActions, shouldRequestInitialBounds: $store.shouldRequestInitialBounds ) .onReceiveMapBounds { diff --git a/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/View/MapViewCoordinator.swift b/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/View/MapViewCoordinator.swift index d020212d..08a282cd 100644 --- a/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/View/MapViewCoordinator.swift +++ b/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/View/MapViewCoordinator.swift @@ -56,7 +56,7 @@ extension MapViewRepresentable { func mapView(_ mapView: NMFMapView, didTapMap latlng: NMGLatLng, point: CGPoint) { if let onMarkerTapped = parent.onMarkerTapped { onMarkerTapped(nil) - parent.mapAction = .deletePath + parent.mapActions.append(.deletePath) } if let _ = focusMarker { deleteSearchResult() @@ -75,7 +75,6 @@ extension MapViewRepresentable { func deleteSearchResult() { deletePath() - parent.focusData = nil if let searchMarker = focusMarker { searchMarker.mapView = nil } @@ -87,7 +86,7 @@ extension MapViewRepresentable { func mapViewCameraIdle(_ mapView: NMFMapView) { // 앱 처음 진입 시 카메라 이동 완료 후 지도 범위 값 가져오도록 처리 if isInitialBounds, parent.shouldRequestInitialBounds { - parent.mapAction = .requestInitialBounds + parent.mapActions.append(.requestInitialBounds) } } @@ -146,9 +145,11 @@ extension MapViewRepresentable { /// 마커 개화 상태 값 업데이트 func updateMarker(state: BloomStatus) { if let activeMarker = activeMarker { + guard selectedPin?.bloomingStatus != state.rawValue else { return } selectedPin?.bloomingStatus = state.rawValue activeMarker.iconImage = NMFOverlayImage(image: state.activeImage) } else if let focusMarker = focusMarker { + guard focusData?.bloomingStatus != state.rawValue else { return } focusMarker.iconImage = NMFOverlayImage(image: state.activeImage) } if let path = paths, diff --git a/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/View/MapViewRepresentable.swift b/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/View/MapViewRepresentable.swift index f9eb2b1d..ac21465d 100644 --- a/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/View/MapViewRepresentable.swift +++ b/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/View/MapViewRepresentable.swift @@ -20,6 +20,9 @@ public enum MapAction: Equatable { case deletePath case moveToUserLocation(Coordinate) case drawPath(FlowerSpotEntity, [Coordinate]) + case changeActiveMarker(FlowerSpotEntity) + case showFocus(FlowerSpotEntity) + case clearFocus } struct MapViewRepresentable: UIViewRepresentable { @@ -29,14 +32,11 @@ struct MapViewRepresentable: UIViewRepresentable { /// 지도를 움직일 경우 현 위치 재검색 버튼 활성화 하기 위한 트리거 @Binding var isCameraMove: Bool - /// 지도에 특정 위치를 표시하기 위한 프로퍼티 - @Binding var focusData: FlowerSpotEntity? - /// 바텀시트 표시 여부 (카메라 중앙 위치 조정용) @Binding var hasBottomSheet: Bool - /// 지도 액션 명령 - @Binding var mapAction: MapAction? + /// 지도 액션 명령 큐 + @Binding var mapActions: [MapAction] /// 초기 bounds 요청 여부 (현재 위치 이동 완료 후 자동 실행용) @Binding var shouldRequestInitialBounds: Bool @@ -90,18 +90,14 @@ struct MapViewRepresentable: UIViewRepresentable { } } - // 특정 위치에 나타날 데이터가 있을 경우 - if let focusData = focusData, context.coordinator.focusData != focusData { - drawPathLine(uiView, data: focusData, for: focusData.path, context: context) - drawFocusMarker(uiView, result: focusData, context: context) - } else if focusData == nil, context.coordinator.focusData != nil { - context.coordinator.deleteSearchResult() - } - - // 액션 기반 명령 처리 - if let action = mapAction { - executeAction(action, on: uiView, context: context) - mapAction = nil + // 액션 큐 기반 명령 처리 + if !mapActions.isEmpty { + let actionsToExecute = mapActions + mapActions.removeAll() // 먼저 큐 클리어 + for action in actionsToExecute { + Logger.log("Executing action: \(action)", level: .debug) + executeAction(action, on: uiView, context: context) + } } } @@ -135,6 +131,16 @@ struct MapViewRepresentable: UIViewRepresentable { case .drawPath(let data, let pathCoordinates): drawPathLine(uiView, data: data, for: pathCoordinates, context: context) + + case let .changeActiveMarker(data): + drawFocusMarker(uiView, result: data, context: context) + + case .showFocus(let data): + drawPathLine(uiView, data: data, for: data.path, context: context) + drawFocusMarker(uiView, result: data, context: context) + + case .clearFocus: + context.coordinator.deleteSearchResult() } } From d89f9271d5eeaedcab195bb0a7cb0c9963bab368 Mon Sep 17 00:00:00 2001 From: ji-yeon224 <69784492+ji-yeon224@users.noreply.github.com> Date: Thu, 26 Feb 2026 16:47:15 +0900 Subject: [PATCH 5/7] =?UTF-8?q?refactor:=20flowerPositions=20=EB=A7=88?= =?UTF-8?q?=EC=BB=A4=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=EC=9D=84=20MapAction=20=EA=B8=B0=EB=B0=98=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - updateMarkers MapAction 추가하여 마커 데이터 변경 처리 - updateUIView의 flowerPositions 바인딩 기반 로직을 액션 기반으로 변경 - storeFlowerData에서 updateMarkers 액션 발송하여 마커 업데이트 - 카메라 이동 여부 로직 유지 (마커 ID 세트 변경 시에만 이동) - 기존 바인딩 기반 로직 제거로 일관된 액션 기반 아키텍처 완성 --- .../Sources/FlowerSpotDetailFeature.swift | 2 - .../Sources/MapFeature/MapFeature.swift | 10 ++-- .../SubFeature/LocationFeature.swift | 3 -- .../MapView/View/MapViewCoordinator.swift | 1 - .../MapView/View/MapViewRepresentable.swift | 50 ++++++++++++------- 5 files changed, 36 insertions(+), 30 deletions(-) diff --git a/Projects/Feature/FlowerSpotDetail/FlowerSpotDetailFeature/Sources/FlowerSpotDetailFeature.swift b/Projects/Feature/FlowerSpotDetail/FlowerSpotDetailFeature/Sources/FlowerSpotDetailFeature.swift index f841014b..29baf857 100644 --- a/Projects/Feature/FlowerSpotDetail/FlowerSpotDetailFeature/Sources/FlowerSpotDetailFeature.swift +++ b/Projects/Feature/FlowerSpotDetail/FlowerSpotDetailFeature/Sources/FlowerSpotDetailFeature.swift @@ -179,7 +179,6 @@ extension FlowerSpotDetailFeature { return fetchDetailInfo(id: id) case let .detailResponse(item, shouldUpdateMap): - let prevData = state.flowerSpotData state.flowerSpotData = item state.spotId = item.id // distance 계산 @@ -195,7 +194,6 @@ extension FlowerSpotDetailFeature { return .concatenate( checkBloomStatus(status: state.flowerSpotData.bloomingStatus), .send(.prefetchImages), - .send(.delegate(.showOnMap(item))), .send(.delegate(.didUpdateFlowerSpot(item))) ) } else { diff --git a/Projects/Feature/Map/MapFeature/Sources/MapFeature/MapFeature.swift b/Projects/Feature/Map/MapFeature/Sources/MapFeature/MapFeature.swift index 102639f8..9d8bfb9f 100644 --- a/Projects/Feature/Map/MapFeature/Sources/MapFeature/MapFeature.swift +++ b/Projects/Feature/Map/MapFeature/Sources/MapFeature/MapFeature.swift @@ -62,6 +62,7 @@ extension MapFeature { state.researchButtonEnable = false if isRequest { analyticsClient.track(MapEvent.researchClicked(currentPage: "map")) + state.flowerSpots.removeAll() return .send(.addMapAction(.requestBounds)) } return .none @@ -165,11 +166,10 @@ extension MapFeature { data.forEach { state.flowerSpots[$0.id] = $0 } - // SearchRegionListFeature가 활성화되어 있으면 데이터 전달 - if state.searchRegionList != nil { - return .send(.searchRegionList(.storeFlowerSpots(data))) - } - return .none + return .concatenate( + .send(.addMapAction(.updateMarkers(state.flowerSpots))), + state.searchRegionList != nil ? .send(.searchRegionList(.storeFlowerSpots(data))) : .none + ) case let .storeUserLocation(location): state.userLocation = location diff --git a/Projects/Feature/Map/MapFeature/Sources/MapFeature/SubFeature/LocationFeature.swift b/Projects/Feature/Map/MapFeature/Sources/MapFeature/SubFeature/LocationFeature.swift index 1af00997..7983b5e0 100644 --- a/Projects/Feature/Map/MapFeature/Sources/MapFeature/SubFeature/LocationFeature.swift +++ b/Projects/Feature/Map/MapFeature/Sources/MapFeature/SubFeature/LocationFeature.swift @@ -77,10 +77,7 @@ extension LocationFeature { extension LocationFeature.Core { - private func moveUserLocation(isCurrentButtonTap: Bool) -> Effect { - - return .run { send in if let location = await locationClient.requestUserLocation() { await send(.saveUserLocation(location)) diff --git a/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/View/MapViewCoordinator.swift b/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/View/MapViewCoordinator.swift index 08a282cd..12be357d 100644 --- a/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/View/MapViewCoordinator.swift +++ b/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/View/MapViewCoordinator.swift @@ -56,7 +56,6 @@ extension MapViewRepresentable { func mapView(_ mapView: NMFMapView, didTapMap latlng: NMGLatLng, point: CGPoint) { if let onMarkerTapped = parent.onMarkerTapped { onMarkerTapped(nil) - parent.mapActions.append(.deletePath) } if let _ = focusMarker { deleteSearchResult() diff --git a/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/View/MapViewRepresentable.swift b/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/View/MapViewRepresentable.swift index ac21465d..79982a88 100644 --- a/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/View/MapViewRepresentable.swift +++ b/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/View/MapViewRepresentable.swift @@ -23,6 +23,7 @@ public enum MapAction: Equatable { case changeActiveMarker(FlowerSpotEntity) case showFocus(FlowerSpotEntity) case clearFocus + case updateMarkers([Int: FlowerSpotEntity]) } struct MapViewRepresentable: UIViewRepresentable { @@ -73,29 +74,12 @@ struct MapViewRepresentable: UIViewRepresentable { } func updateUIView(_ uiView: NMFNaverMapView, context: Context) { - // 마커 데이터가 변경되었을 때 마커 업데이트 - if context.coordinator.currentFlowerPositions != flowerPositions { - // 마커 ID 세트가 변경된 경우에만 카메라 이동 (데이터만 갱신된 경우 스킵) - let shouldMoveCamera = Set(context.coordinator.currentFlowerPositions.keys) != Set(flowerPositions.keys) - - // 기존 마커 삭제 - if !context.coordinator.markers.isEmpty { - context.coordinator.deleteAllMarkers() - } - - if !flowerPositions.isEmpty { - // 새로운 마커 표시 - presentMarkers(uiView, flowers: flowerPositions, context: context, shouldMoveCamera: shouldMoveCamera) - context.coordinator.currentFlowerPositions = flowerPositions - } - } // 액션 큐 기반 명령 처리 if !mapActions.isEmpty { let actionsToExecute = mapActions mapActions.removeAll() // 먼저 큐 클리어 for action in actionsToExecute { - Logger.log("Executing action: \(action)", level: .debug) executeAction(action, on: uiView, context: context) } } @@ -106,7 +90,11 @@ struct MapViewRepresentable: UIViewRepresentable { } /// 액션 기반 명령 실행 - private func executeAction(_ action: MapAction, on uiView: NMFNaverMapView, context: Context) { + private func executeAction( + _ action: MapAction, + on uiView: NMFNaverMapView, + context: Context + ) { switch action { case .requestBounds: if !context.coordinator.isInitialBounds { @@ -141,6 +129,10 @@ struct MapViewRepresentable: UIViewRepresentable { case .clearFocus: context.coordinator.deleteSearchResult() + + case .updateMarkers(let flowers): + updateMarkersAction(uiView, flowers: flowers, context: context) + } } @@ -150,6 +142,27 @@ struct MapViewRepresentable: UIViewRepresentable { extension MapViewRepresentable { + /// 마커 데이터 업데이트 액션 + private func updateMarkersAction( + _ view: NMFNaverMapView, + flowers: [Int: FlowerSpotEntity], + context: Context + ) { + flowerPositions = flowers + // 기존 마커 삭제 + if !context.coordinator.markers.isEmpty { + context.coordinator.deleteAllMarkers() + } + + if !flowers.isEmpty { + // 새로운 마커 표시 + let shouldMoveCamera = Set(context.coordinator.currentFlowerPositions.keys) != Set(flowerPositions.keys) + presentMarkers(view, flowers: flowers, context: context, shouldMoveCamera: shouldMoveCamera) + context.coordinator.currentFlowerPositions = flowers + + } + } + /// 특정 위치에 마커를 표시 func drawFocusMarker(_ view: NMFNaverMapView, result: FlowerSpotEntity, context: Context) { // 중복 그리기 방지 @@ -302,7 +315,6 @@ extension MapViewRepresentable { /// 지도에 올라와있는 마커 삭제 func deleteDrawMarker(context: Context) { if !context.coordinator.markers.isEmpty { - flowerPositions.removeAll() context.coordinator.deleteAllMarkers() } } From f3ada06fe689c83b254b5ce527281afbdd1325e7 Mon Sep 17 00:00:00 2001 From: ji-yeon224 <69784492+ji-yeon224@users.noreply.github.com> Date: Thu, 26 Feb 2026 16:56:20 +0900 Subject: [PATCH 6/7] =?UTF-8?q?refactor:=20MapAction=EC=9D=84=20=EB=B3=84?= =?UTF-8?q?=EB=8F=84=20=ED=8C=8C=EC=9D=BC=EB=A1=9C=20=EB=B6=84=EB=A6=AC=20?= =?UTF-8?q?=EB=B0=8F=20=EC=83=81=EC=84=B8=20=EC=A3=BC=EC=84=9D=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MapViewRepresentable.swift에서 MapAction 열거형을 MapViewAction.swift로 분리 - 각 MapAction case에 대한 상세한 용도와 매개변수 설명 주석 추가 - 코드 구조 개선을 통한 가독성 및 유지보수성 향상 --- .../Component/BloomState/BloomStatus.swift | 2 +- .../Sources/MapView/View/MapViewAction.swift | 58 +++++++++++++++++++ .../MapView/View/MapViewRepresentable.swift | 13 ----- 3 files changed, 59 insertions(+), 14 deletions(-) create mode 100644 Projects/Feature/Map/MapFeatureInterface/Sources/MapView/View/MapViewAction.swift diff --git a/Projects/DesignKit/Sources/Component/BloomState/BloomStatus.swift b/Projects/DesignKit/Sources/Component/BloomState/BloomStatus.swift index c09b295a..b6b94678 100644 --- a/Projects/DesignKit/Sources/Component/BloomState/BloomStatus.swift +++ b/Projects/DesignKit/Sources/Component/BloomState/BloomStatus.swift @@ -9,7 +9,7 @@ import Foundation import SwiftUI -public enum BloomStatus: String, Sendable { +public enum BloomStatus: String, Sendable, Equatable { case little = "LITTLE" case bloomed = "BLOOMED" case withered = "WITHERED" diff --git a/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/View/MapViewAction.swift b/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/View/MapViewAction.swift new file mode 100644 index 00000000..abb8186e --- /dev/null +++ b/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/View/MapViewAction.swift @@ -0,0 +1,58 @@ +// +// MapViewAction.swift +// MapFeature +// +// Created by Jiyeon on 3/14/25. +// Copyright © 2025 com.yongin.pida. All rights reserved. +// + +import FlowerSpotClient +import BloomingClient +import Shared +import DesignKit + +/// 지도 뷰에서 발생하는 액션들을 정의하는 열거형 +public enum MapAction: Equatable { + /// 현재 화면에 보이는 지도 영역의 좌표 범위를 요청 + /// - Note: 지도 이동 후 해당 영역의 꽃 명소 데이터를 가져올 때 사용 + case requestBounds + + /// 앱 시작 시 또는 최초 위치 설정 후 초기 지도 영역 요청 + /// - Note: 사용자 현재 위치로 이동 완료 후 자동으로 실행 + case requestInitialBounds + + /// 특정 꽃 명소의 개화 상태에 따라 마커 표시 상태를 업데이트 + /// - Parameter BloomStatus: 개화 상태 (피지않음, 피기시작, 만개, 져감) + case updateMarkerStatus(BloomStatus) + + /// 지도에 그려진 경로선과 마커들을 모두 삭제 + /// - Note: 새로운 경로를 그리기 전이나 초기화할 때 사용 + case deletePath + + /// 사용자의 현재 위치로 지도 카메라를 이동 + /// - Parameter Coordinate: 이동할 좌표 (위도, 경도) + case moveToUserLocation(Coordinate) + + /// 특정 꽃 명소까지의 경로를 지도에 그리기 + /// - Parameters: + /// - FlowerSpotEntity: 목적지 꽃 명소 정보 + /// - [Coordinate]: 경로를 구성하는 좌표 배열 + case drawPath(FlowerSpotEntity, [Coordinate]) + + /// 활성화된 마커를 변경 (선택된 상태로 표시) + /// - Parameter FlowerSpotEntity: 활성화할 꽃 명소 정보 + case changeActiveMarker(FlowerSpotEntity) + + /// 특정 꽃 명소에 포커스 (경로 그리기 + 마커 활성화) + /// - Parameter FlowerSpotEntity: 포커스할 꽃 명소 정보 + /// - Note: 검색 결과나 상세 정보에서 해당 위치를 강조할 때 사용 + case showFocus(FlowerSpotEntity) + + /// 현재 포커스된 요소들을 모두 제거 + /// - Note: 검색 결과나 상세 정보 해제 시 사용 + case clearFocus + + /// 지도에 표시할 꽃 명소 마커들을 업데이트 + /// - Parameter [Int: FlowerSpotEntity]: 꽃 명소 ID를 키로 하는 딕셔너리 + case updateMarkers([Int: FlowerSpotEntity]) +} diff --git a/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/View/MapViewRepresentable.swift b/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/View/MapViewRepresentable.swift index 79982a88..d7411df1 100644 --- a/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/View/MapViewRepresentable.swift +++ b/Projects/Feature/Map/MapFeatureInterface/Sources/MapView/View/MapViewRepresentable.swift @@ -13,19 +13,6 @@ import FlowerSpotClient import BloomingClient import Shared -public enum MapAction: Equatable { - case requestBounds - case requestInitialBounds - case updateMarkerStatus(BloomStatus) - case deletePath - case moveToUserLocation(Coordinate) - case drawPath(FlowerSpotEntity, [Coordinate]) - case changeActiveMarker(FlowerSpotEntity) - case showFocus(FlowerSpotEntity) - case clearFocus - case updateMarkers([Int: FlowerSpotEntity]) -} - struct MapViewRepresentable: UIViewRepresentable { /// 지도에 보여줄 데이터 @Binding var flowerPositions: [Int: FlowerSpotEntity] From ab30b789f0a2ec6ab20c0bebe7bd2886a9f363d9 Mon Sep 17 00:00:00 2001 From: ji-yeon224 <69784492+ji-yeon224@users.noreply.github.com> Date: Thu, 26 Feb 2026 17:08:22 +0900 Subject: [PATCH 7/7] =?UTF-8?q?chore:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20action=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Map/MapFeature/Sources/MapFeature/MapFeature.swift | 4 ---- .../MapFeatureInterface/Sources/MapFeature/MapFeature.swift | 1 - 2 files changed, 5 deletions(-) diff --git a/Projects/Feature/Map/MapFeature/Sources/MapFeature/MapFeature.swift b/Projects/Feature/Map/MapFeature/Sources/MapFeature/MapFeature.swift index 9d8bfb9f..e97e0e1d 100644 --- a/Projects/Feature/Map/MapFeature/Sources/MapFeature/MapFeature.swift +++ b/Projects/Feature/Map/MapFeature/Sources/MapFeature/MapFeature.swift @@ -249,10 +249,6 @@ extension MapFeature { state.mapActions.append(action) return .none - case .clearMapActions: - state.mapActions.removeAll() - return .none - case .binding, .delegate, .alertAcceptTapped, .location, .searchRegionList, .mapSearch: return .none diff --git a/Projects/Feature/Map/MapFeatureInterface/Sources/MapFeature/MapFeature.swift b/Projects/Feature/Map/MapFeatureInterface/Sources/MapFeature/MapFeature.swift index dd88e490..002e2d51 100644 --- a/Projects/Feature/Map/MapFeatureInterface/Sources/MapFeature/MapFeature.swift +++ b/Projects/Feature/Map/MapFeatureInterface/Sources/MapFeature/MapFeature.swift @@ -91,7 +91,6 @@ public struct MapFeature { case requestMapBounds(Bool) case addMapAction(MapAction) - case clearMapActions case markerTapped(id: Int?) case fetchPathLines(Int) case fetchDetailInfo(Int)