diff --git a/SUSA24-iOS/SUSA24-iOS/Resources/Assets.xcassets/icn_home.imageset/Contents.json b/SUSA24-iOS/SUSA24-iOS/Resources/Assets.xcassets/icn_home.imageset/Contents.json index 53ea4d56..938c0552 100644 --- a/SUSA24-iOS/SUSA24-iOS/Resources/Assets.xcassets/icn_home.imageset/Contents.json +++ b/SUSA24-iOS/SUSA24-iOS/Resources/Assets.xcassets/icn_home.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "home.svg", + "filename" : "icn_home.svg", "idiom" : "universal" } ], diff --git a/SUSA24-iOS/SUSA24-iOS/Resources/Assets.xcassets/icn_home.imageset/home.svg b/SUSA24-iOS/SUSA24-iOS/Resources/Assets.xcassets/icn_home.imageset/home.svg deleted file mode 100644 index e185392d..00000000 --- a/SUSA24-iOS/SUSA24-iOS/Resources/Assets.xcassets/icn_home.imageset/home.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/SUSA24-iOS/SUSA24-iOS/Resources/Assets.xcassets/icn_home.imageset/icn_home.svg b/SUSA24-iOS/SUSA24-iOS/Resources/Assets.xcassets/icn_home.imageset/icn_home.svg new file mode 100644 index 00000000..fc1e9428 --- /dev/null +++ b/SUSA24-iOS/SUSA24-iOS/Resources/Assets.xcassets/icn_home.imageset/icn_home.svg @@ -0,0 +1,4 @@ + + + + diff --git a/SUSA24-iOS/SUSA24-iOS/Resources/Literals/Localizable.xcstrings b/SUSA24-iOS/SUSA24-iOS/Resources/Literals/Localizable.xcstrings index a7965688..f7c76368 100644 --- a/SUSA24-iOS/SUSA24-iOS/Resources/Literals/Localizable.xcstrings +++ b/SUSA24-iOS/SUSA24-iOS/Resources/Literals/Localizable.xcstrings @@ -1437,6 +1437,9 @@ }, "전달된 문자 메시지에 포함된 주소를 추출하여 케이스에 저장합니다." : { + }, + "취소" : { + } }, "version" : "1.1" diff --git a/SUSA24-iOS/SUSA24-iOS/Resources/Literals/Notification.swift b/SUSA24-iOS/SUSA24-iOS/Resources/Literals/Notification.swift index c7b52c91..0bc7648a 100644 --- a/SUSA24-iOS/SUSA24-iOS/Resources/Literals/Notification.swift +++ b/SUSA24-iOS/SUSA24-iOS/Resources/Literals/Notification.swift @@ -9,4 +9,5 @@ import Foundation extension Notification.Name { static let resetDetentToMid = Notification.Name("resetDetentToMid") + static let resetDetentToShort = Notification.Name("resetDetentToShort") } diff --git a/SUSA24-iOS/SUSA24-iOS/Sources/Application/Coordinator/AppCoordinator.swift b/SUSA24-iOS/SUSA24-iOS/Sources/Application/Coordinator/AppCoordinator.swift index f4456c19..14db7f9b 100644 --- a/SUSA24-iOS/SUSA24-iOS/Sources/Application/Coordinator/AppCoordinator.swift +++ b/SUSA24-iOS/SUSA24-iOS/Sources/Application/Coordinator/AppCoordinator.swift @@ -5,55 +5,26 @@ // Created by mini on 10/29/25. // -import Combine import SwiftUI @MainActor @Observable final class AppCoordinator { - var path = NavigationPath() - private var routes: [AppRoute] = [] - private(set) var currentRoute: AppRoute? + var path: [AppRoute] = [] func push(_ route: AppRoute) { path.append(route) - routes.append(route) - syncRoute() } func pop() { path.removeLast() - if !routes.isEmpty { - routes.removeLast() - } - syncRoute() } func popToRoot() { path.removeLast(path.count) - routes.removeAll() - syncRoute() } func popToDepth(_ depth: Int) { path.removeLast(depth) - if routes.count >= depth { - routes.removeLast(depth) - } - syncRoute() - } - - func replaceLast(_ route: AppRoute) { - if !routes.isEmpty { - path.removeLast() - routes.removeLast() - } - path.append(route) - routes.append(route) - syncRoute() - } - - private func syncRoute() { - currentRoute = routes.last } } diff --git a/SUSA24-iOS/SUSA24-iOS/Sources/Application/Coordinator/RootView.swift b/SUSA24-iOS/SUSA24-iOS/Sources/Application/Coordinator/RootView.swift index 2c721132..86bc3d13 100644 --- a/SUSA24-iOS/SUSA24-iOS/Sources/Application/Coordinator/RootView.swift +++ b/SUSA24-iOS/SUSA24-iOS/Sources/Application/Coordinator/RootView.swift @@ -71,16 +71,12 @@ struct RootView: View { moduleFactory.makeTrackingView(caseID: caseID, context: context) } } - .onChange(of: coordinator.currentRoute) { _, newRoute in - guard let newRoute else { + .onChange(of: coordinator.path) { + guard let lastRoute = coordinator.path.last else { tabBarVisibility.hide() return } - if newRoute.useTabBar { - tabBarVisibility.show() - } else { - tabBarVisibility.hide() - } + lastRoute.useTabBar ? tabBarVisibility.show() : tabBarVisibility.hide() } } } diff --git a/SUSA24-iOS/SUSA24-iOS/Sources/Presentation/MainTabScene/MainTabView.swift b/SUSA24-iOS/SUSA24-iOS/Sources/Presentation/MainTabScene/MainTabView.swift index 1d0768e5..bfd359d8 100644 --- a/SUSA24-iOS/SUSA24-iOS/Sources/Presentation/MainTabScene/MainTabView.swift +++ b/SUSA24-iOS/SUSA24-iOS/Sources/Presentation/MainTabScene/MainTabView.swift @@ -146,6 +146,14 @@ struct MainTabView: View if isShortDetent { timeLineStore.send(.clearCellFilter) } + + // 타임라인 시트 상태를 MapFeature로 전달 (PlaceInfoSheet 표시 제어용) + mapStore.send(.updateTimelineSheetState(isMinimized: isShortDetent)) + } + .onAppear { + // 초기 상태를 즉시 전달 + let isShortDetent = (selectedDetent == mapShortDetent || selectedDetent == otherDetent) + mapStore.send(.updateTimelineSheetState(isMinimized: isShortDetent)) } .onChange(of: store.state.selectedTab) { _, newTab in if newTab == .map { @@ -170,6 +178,13 @@ struct MainTabView: View .onReceive(NotificationCenter.default.publisher(for: .resetDetentToMid)) { _ in selectedDetent = mapMidDetnet } + .onReceive(NotificationCenter.default.publisher(for: .resetDetentToShort)) { _ in + // 타임라인 시트가 올라와 있을 때만 최소화 + if selectedDetent != mapShortDetent { + selectedDetent = mapShortDetent + // clearCellFilter는 onChange(of: selectedDetent)에서 자동 호출됨 + } + } } } diff --git a/SUSA24-iOS/SUSA24-iOS/Sources/Presentation/MapScene/MapFeature.swift b/SUSA24-iOS/SUSA24-iOS/Sources/Presentation/MapScene/MapFeature.swift index 46c3a525..de064ba8 100644 --- a/SUSA24-iOS/SUSA24-iOS/Sources/Presentation/MapScene/MapFeature.swift +++ b/SUSA24-iOS/SUSA24-iOS/Sources/Presentation/MapScene/MapFeature.swift @@ -104,6 +104,12 @@ struct MapFeature: DWReducer { var selectedPlaceInfo: PlaceInfo? /// 선택된 위치의 기존 핀 정보가 있는가? var existingLocation: Location? + /// 타임라인 시트가 최소 높이인지 여부입니다. + /// - `true`: 타임라인 시트가 최소 높이 (PlaceInfoSheet 표시 가능) + /// - `false`: 타임라인 시트가 올라와 있음 (PlaceInfoSheet 표시 안 함) + var isTimelineSheetMinimized: Bool = true + /// 마커 선택 해제 트리거 (PlaceInfoSheet 닫힐 때 사용) + var deselectMarkerTrigger: UUID? // MARK: - Pin Add/Edit @@ -168,6 +174,9 @@ struct MapFeature: DWReducer { case showPlaceInfo(PlaceInfo) /// 위치정보 시트를 닫는 액션입니다. 사용자가 시트를 드래그 내려 닫거나 Close 버튼을 누를 때 호출됩니다. case hidePlaceInfo + /// 타임라인 시트 상태를 업데이트하는 액션입니다. + /// - Parameter isMinimized: 타임라인 시트가 최소 높이인지 여부 + case updateTimelineSheetState(isMinimized: Bool) // MARK: - CCTV 데이터 로드 @@ -334,6 +343,10 @@ struct MapFeature: DWReducer { // MARK: - 위치정보 시트 관련 액션 처리 case let .mapTapped(latlng): + // 타임라인 시트가 올라와 있으면 PlaceInfoSheet 표시 안 함 (API 요청도 하지 않음) + // resetDetentToShort Notification으로 시트는 이미 최소화 요청됨 + guard state.isTimelineSheetMinimized else { return .none } + // 위치정보 시트 표시 및 로딩 상태 설정 // 새 위치 탭하면 기존 핀 정보는 즉시 비워야 함 state.existingLocation = nil @@ -372,6 +385,9 @@ struct MapFeature: DWReducer { // existingLocation 찾기 (주소 매칭) state.existingLocation = findExistingLocation(for: placeInfo, in: state.locations) + // PlaceInfoSheet 표시 시 타임라인 시트를 최소화 + NotificationCenter.default.post(name: .resetDetentToShort, object: nil) + return .none case let .userLocationMarkerTapped(locationId): @@ -380,6 +396,9 @@ struct MapFeature: DWReducer { return .none } + // PlaceInfoSheet 표시 시 타임라인 시트를 최소화 + NotificationCenter.default.post(name: .resetDetentToShort, object: nil) + state.existingLocation = location state.isEditMode = false state.selectedCoordinate = MapCoordinate( @@ -413,6 +432,14 @@ struct MapFeature: DWReducer { state.isPlaceInfoSheetPresented = false state.isPlaceInfoLoading = false state.selectedPlaceInfo = nil + // 마커 선택 해제 트리거 + state.deselectMarkerTrigger = UUID() + // PlaceInfoSheet 닫힐 때도 타임라인 시트를 최소화 상태로 유지 + NotificationCenter.default.post(name: .resetDetentToShort, object: nil) + return .none + + case let .updateTimelineSheetState(isMinimized): + state.isTimelineSheetMinimized = isMinimized return .none case let .cameraIdle(bounds, zoomLevel): diff --git a/SUSA24-iOS/SUSA24-iOS/Sources/Presentation/MapScene/MapView.swift b/SUSA24-iOS/SUSA24-iOS/Sources/Presentation/MapScene/MapView.swift index 2e166d1d..8ae70da3 100644 --- a/SUSA24-iOS/SUSA24-iOS/Sources/Presentation/MapScene/MapView.swift +++ b/SUSA24-iOS/SUSA24-iOS/Sources/Presentation/MapScene/MapView.swift @@ -79,9 +79,12 @@ struct MapView: View { onUserLocationMarkerTapped: { locationId in store.send(.userLocationMarkerTapped(locationId)) }, + isTimelineSheetMinimized: store.state.isTimelineSheetMinimized, onMapTapped: { latlng in + // MapFeature에서 타임라인 시트 상태를 체크하여 처리 store.send(.mapTapped(latlng)) }, + deselectMarkerTrigger: store.state.deselectMarkerTrigger, onCameraIdle: { bounds, zoomLevel in store.send(.cameraIdle(bounds: bounds, zoomLevel: zoomLevel)) }, diff --git a/SUSA24-iOS/SUSA24-iOS/Sources/Presentation/ScanLoadScene/ScanLoadView.swift b/SUSA24-iOS/SUSA24-iOS/Sources/Presentation/ScanLoadScene/ScanLoadView.swift index fa03a805..2ff50cf3 100644 --- a/SUSA24-iOS/SUSA24-iOS/Sources/Presentation/ScanLoadScene/ScanLoadView.swift +++ b/SUSA24-iOS/SUSA24-iOS/Sources/Presentation/ScanLoadScene/ScanLoadView.swift @@ -67,21 +67,19 @@ struct ScanLoadView: View { showRetryAlert = true } } - .dwAlert( + .alert( + String(localized: .scanLoadFailedTitle), isPresented: $showRetryAlert, - title: String(localized: .scanLoadFailedTitle), - message: String(localized: .scanLoadFailedContent), - primaryButton: DWAlertButton( - title: String(localized: .scanLoadTry), - style: .default - ) { - handleRetry() + actions: { + Button(String(localized: .scanLoadTry), role: .confirm) { + handleRetry() + } + Button("취소", role: .cancel) { + handleCancel() + } }, - secondaryButton: DWAlertButton( - title: "취소", - style: .cancel - ) { - handleCancel() + message: { + Text(String(localized: .scanLoadFailedContent)) } ) } @@ -89,33 +87,21 @@ struct ScanLoadView: View { private extension ScanLoadView { func navigateToScanList() { - Task { @MainActor in - try? await Task.sleep(for: .seconds(0.5)) - coordinator.replaceLast( - .scanListScene( - caseID: caseID, - scanResults: store.state.scanResults - ) + coordinator.push( + .scanListScene( + caseID: caseID, + scanResults: store.state.scanResults ) - } + ) } func handleRetry() { camera.clearAllPhotos() - - Task { @MainActor in - try? await Task.sleep(for: .seconds(0.3)) - coordinator.pop() - } + coordinator.pop() } func handleCancel() { camera.clearAllPhotos() - - if coordinator.path.count >= 2 { - coordinator.popToDepth(2) - } else { - coordinator.popToRoot() - } + coordinator.popToDepth(2) } } diff --git a/SUSA24-iOS/SUSA24-iOS/Sources/Util/NaverMap/Manager/CaseLocationMarkerManager.swift b/SUSA24-iOS/SUSA24-iOS/Sources/Util/NaverMap/Manager/CaseLocationMarkerManager.swift index 361e2a78..2b737cac 100644 --- a/SUSA24-iOS/SUSA24-iOS/Sources/Util/NaverMap/Manager/CaseLocationMarkerManager.swift +++ b/SUSA24-iOS/SUSA24-iOS/Sources/Util/NaverMap/Manager/CaseLocationMarkerManager.swift @@ -64,10 +64,7 @@ final class CaseLocationMarkerManager { guard !cellCounts.isEmpty else { return } for (id, count) in cellCounts { - guard let overlay = markers[id] else { continue } - - // 선택된 마커는 큰 핀 유지 - if id == selectedMarkerId { continue } + guard let overlay = markers[id], !isSelectedMarker(id) else { continue } let icon = await MarkerImageCache.shared.image(for: .cellWithCount(count: count)) overlay.iconImage = NMFOverlayImage(image: icon) @@ -80,10 +77,7 @@ final class CaseLocationMarkerManager { /// - Parameter mapView: 네이버 지도 뷰 func resetVisitFrequency(on mapView: NMFMapView) async { for (id, overlay) in markers { - guard case .cellWithCount = markerTypes[id] else { continue } - - // 선택된 마커는 큰 핀 유지 - if id == selectedMarkerId { continue } + guard case .cellWithCount = markerTypes[id], !isSelectedMarker(id) else { continue } let icon = await MarkerImageCache.shared.image(for: .cell(isVisited: true)) overlay.iconImage = NMFOverlayImage(image: icon) @@ -100,6 +94,22 @@ final class CaseLocationMarkerManager { } } + /// 줌 레벨에 따라 사용자 위치 마커(home, work, custom)의 캡션 표시 여부를 업데이트합니다. + /// - Parameters: + /// - locations: 위치 데이터 배열 (캡션 텍스트 참조용) + /// - zoomLevel: 현재 줌 레벨 + /// - threshold: 캡션을 숨길 줌 레벨 임계값 (기본값: 11.5) + func updateCaptionVisibility(locations: [Location], zoomLevel: Double, threshold: Double = 11.5) { + let shouldShowCaption = zoomLevel > threshold + let locationMap = Dictionary(uniqueKeysWithValues: locations.map { ($0.id.uuidString, $0.title) }) + + for (id, marker) in markers { + guard let markerType = markerTypes[id], markerType.isUserLocation else { continue } + guard let title = locationMap[id], let unwrappedTitle = title, !unwrappedTitle.isEmpty else { continue } + marker.captionText = shouldShowCaption ? unwrappedTitle : "" + } + } + /// 마커 선택 해제 func deselectMarker(on _: NMFMapView) async { guard let selectedId = selectedMarkerId, @@ -120,12 +130,48 @@ final class CaseLocationMarkerManager { let coordinate: MapCoordinate /// 사용자 위치 마커의 색상 (home / work / custom 에서만 사용) let pinColor: PinColorType? + /// 핀 이름 (캡션으로 표시) + let title: String? var markerType: MarkerType } // MARK: - Private Methods + /// 선택된 마커인지 확인합니다. + private func isSelectedMarker(_ id: String) -> Bool { + id == selectedMarkerId + } + + /// 마커에 캡션을 설정합니다. + private func setCaption(on marker: NMFMarker, title: String?) { + if let title, !title.isEmpty { + marker.captionText = title + marker.captionRequestedWidth = 100 + } else { + marker.captionText = "" + } + } + + /// 사용자 위치 마커 아이콘을 생성합니다. + private func createUserLocationIcon(for markerType: MarkerType, color: PinColorType?) async -> UIImage { + if let color { + await MarkerImageCache.shared.userLocationImage(for: markerType, color: color) + } else { + await MarkerImageCache.shared.image(for: markerType) + } + } + + /// LocationType을 MarkerType으로 변환합니다. + private func markerType(from locationType: LocationType) -> MarkerType? { + switch locationType { + case .home: .home + case .work: .work + case .custom: .custom + case .cell: nil + } + } + private func createMarker( for marker: MarkerModel, on mapView: NMFMapView, @@ -137,14 +183,18 @@ final class CaseLocationMarkerManager { lat: marker.coordinate.latitude, lng: marker.coordinate.longitude ) - let icon: UIImage = if marker.markerType.isUserLocation, let color = marker.pinColor { - await MarkerImageCache.shared.userLocationImage(for: marker.markerType, color: color) + let icon: UIImage = if marker.markerType.isUserLocation { + await createUserLocationIcon(for: marker.markerType, color: marker.pinColor) } else { await MarkerImageCache.shared.image(for: marker.markerType) } overlay.iconImage = NMFOverlayImage(image: icon) overlay.width = CGFloat(NMF_MARKER_SIZE_AUTO) overlay.height = CGFloat(NMF_MARKER_SIZE_AUTO) + + // 핀 이름을 캡션으로 표시 + setCaption(on: overlay, title: marker.title) + overlay.mapView = mapView // 탭 핸들러 등록 (선택 가능한 마커만) @@ -247,34 +297,19 @@ final class CaseLocationMarkerManager { let longitude = location.pointLongitude guard latitude != 0, longitude != 0 else { continue } + let locationType = LocationType(location.locationType) + guard let markerType = markerType(from: locationType) else { continue } + let coordinate = MapCoordinate(latitude: latitude, longitude: longitude) let pinColor = PinColorType(location.colorType) - switch LocationType(location.locationType) { - case .home: - markers.append(MarkerModel( - id: location.id.uuidString, - coordinate: coordinate, - pinColor: pinColor, - markerType: .home - )) - case .work: - markers.append(MarkerModel( - id: location.id.uuidString, - coordinate: coordinate, - pinColor: pinColor, - markerType: .work - )) - case .custom: - markers.append(MarkerModel( - id: location.id.uuidString, - coordinate: coordinate, - pinColor: pinColor, - markerType: .custom - )) - case .cell: - break - } + markers.append(MarkerModel( + id: location.id.uuidString, + coordinate: coordinate, + pinColor: pinColor, + title: location.title, + markerType: markerType + )) } // 2. 기지국 방문 빈도 계산 (유틸리티 사용) @@ -288,6 +323,7 @@ final class CaseLocationMarkerManager { id: key, coordinate: coordinate, pinColor: nil, + title: nil, markerType: .cell(isVisited: true) ) ) @@ -348,6 +384,8 @@ final class CaseLocationMarkerManager { markerColors[markerInfo.id] = markerInfo.pinColor } } + // 핀 이름 캡션 업데이트 + setCaption(on: overlay, title: markerInfo.title) // 색상이 동일하면 큰 핀 유지 (continue) continue } @@ -359,8 +397,8 @@ final class CaseLocationMarkerManager { let typeChanged = markerTypes[markerInfo.id] != markerInfo.markerType if isUserLocation || typeChanged { - let icon: UIImage = if isUserLocation, let color = markerInfo.pinColor { - await MarkerImageCache.shared.userLocationImage(for: markerInfo.markerType, color: color) + let icon: UIImage = if isUserLocation { + await createUserLocationIcon(for: markerInfo.markerType, color: markerInfo.pinColor) } else { await MarkerImageCache.shared.image(for: markerInfo.markerType) } @@ -370,6 +408,9 @@ final class CaseLocationMarkerManager { markerColors[markerInfo.id] = markerInfo.pinColor } + // 핀 이름 캡션 업데이트 + setCaption(on: overlay, title: markerInfo.title) + // 탭 핸들러 등록 if isSelectableMarker(markerInfo.markerType) { overlay.touchHandler = { [weak self] _ in diff --git a/SUSA24-iOS/SUSA24-iOS/Sources/Util/NaverMap/NaverMapView.swift b/SUSA24-iOS/SUSA24-iOS/Sources/Util/NaverMap/NaverMapView.swift index 90bf4583..2ac996ad 100644 --- a/SUSA24-iOS/SUSA24-iOS/Sources/Util/NaverMap/NaverMapView.swift +++ b/SUSA24-iOS/SUSA24-iOS/Sources/Util/NaverMap/NaverMapView.swift @@ -37,11 +37,17 @@ struct NaverMapView: UIViewRepresentable { /// - `true`: 지도 터치 이벤트 처리 (시트가 최소 높이일 때) /// - `false`: 지도 터치 이벤트 차단 (시트가 중간/최대 높이일 때) var isMapTouchEnabled: Bool = true + /// 타임라인 시트가 최소 높이인지 여부입니다. + /// - `true`: 타임라인 시트가 최소 높이 (PlaceInfoSheet 표시 가능) + /// - `false`: 타임라인 시트가 올라와 있음 (PlaceInfoSheet 표시 안 함) + var isTimelineSheetMinimized: Bool = true // MARK: 사용자 상호작용 콜백 /// 지도 터치 이벤트를 상위 모듈로 전달하는 콜백입니다. var onMapTapped: ((NMGLatLng) -> Void)? + /// 마커 선택 해제 트리거 (PlaceInfoSheet 닫힐 때 사용) + var deselectMarkerTrigger: UUID? /// 카메라 이동이 멈췄을 때 호출되는 콜백입니다. var onCameraIdle: ((MapBounds, Double) -> Void)? /// 기지국 데이터 @@ -145,7 +151,8 @@ struct NaverMapView: UIViewRepresentable { context.coordinator.updateCaseLocations( locations: locations, visitFrequencyEnabled: isVisitFrequencyEnabled, - isVisible: shouldShowMarkers, + isVisible: true, // caseLocation 마커(home, work, 방문 셀)는 줌 레벨과 무관하게 항상 표시 + zoomLevel: zoomLevel, on: uiView ) context.coordinator.updateCellRangeOverlay( @@ -161,10 +168,18 @@ struct NaverMapView: UIViewRepresentable { ) context.coordinator.updateMarkerVisibility( - isCaseLocationVisible: shouldShowMarkers, + isCaseLocationVisible: true, // caseLocation 마커는 줌 레벨과 무관하게 항상 표시 isCellMarkerVisible: cellLayerVisible, isCCTVVisible: cctvLayerVisible ) + + // 마커 선택 해제 트리거 처리 + if let trigger = deselectMarkerTrigger, trigger != context.coordinator.lastDeselectMarkerTrigger { + context.coordinator.lastDeselectMarkerTrigger = trigger + Task { @MainActor in + await context.coordinator.deselectMarker() + } + } } // MARK: - Coordinator @@ -179,6 +194,7 @@ struct NaverMapView: UIViewRepresentable { private let infrastructureManager: InfrastructureMarkerManager private let caseLocationMarkerManager: CaseLocationMarkerManager + var lastDeselectMarkerTrigger: UUID? private var lastCellStationsHash: Int? private var lastLocationsHash: Int? private var lastCellRangeConfig: CellRangeConfig? @@ -273,23 +289,38 @@ struct NaverMapView: UIViewRepresentable { } /// 지도 터치 이벤트를 SwiftUI 상위 모듈로 전달합니다. - func mapView(_ mapView: NMFMapView, didTapMap latlng: NMGLatLng, point _: CGPoint) { + func mapView(_: NMFMapView, didTapMap latlng: NMGLatLng, point _: CGPoint) { // 지도 터치가 비활성화되어 있으면 이벤트를 처리하지 않습니다. guard parent.isMapTouchEnabled else { return } // 마커 선택 해제 Task { @MainActor in - await caseLocationMarkerManager.deselectMarker(on: mapView) + await deselectMarker() } + + // 타임라인 시트를 최소 높이로 내리도록 요청 (detent 제어) + NotificationCenter.default.post(name: .resetDetentToShort, object: nil) + + // 타임라인 시트가 올라와 있으면 PlaceInfoSheet 표시 안 함 + guard parent.isTimelineSheetMinimized else { return } + // PlaceInfoSheet 표시를 위한 콜백 호출 parent.onMapTapped?(latlng) } + /// 마커 선택 해제를 수행합니다. + @MainActor + func deselectMarker() async { + guard let mapView else { return } + await caseLocationMarkerManager.deselectMarker(on: mapView) + } + @MainActor func updateCaseLocations( locations: [Location], visitFrequencyEnabled: Bool, isVisible: Bool, + zoomLevel: Double, on mapView: NMFMapView ) { var hasher = Hasher() @@ -333,12 +364,16 @@ struct NaverMapView: UIViewRepresentable { // 마커 업데이트 완료 후 가시성 재적용 caseLocationMarkerManager.setVisibility(isVisible) + // 줌 레벨에 따른 캡션 가시성 업데이트 + caseLocationMarkerManager.updateCaptionVisibility(locations: locations, zoomLevel: zoomLevel) } lastLocationsHash = newHash } else { // 해시가 같아도 가시성은 다시 적용 (줌 레벨 변경 시) caseLocationMarkerManager.setVisibility(isVisible) + // 줌 레벨에 따른 캡션 가시성 업데이트 + caseLocationMarkerManager.updateCaptionVisibility(locations: locations, zoomLevel: zoomLevel) } }