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)
}
}