11package com.project200.feature.matching.map
22
3+ import android.Manifest
4+ import android.annotation.SuppressLint
5+ import android.content.pm.PackageManager
36import android.view.View
47import android.widget.Toast
8+ import androidx.activity.result.contract.ActivityResultContracts
9+ import androidx.core.content.ContextCompat
510import androidx.core.view.isVisible
611import androidx.fragment.app.viewModels
712import androidx.lifecycle.Lifecycle
813import androidx.lifecycle.lifecycleScope
914import androidx.lifecycle.repeatOnLifecycle
1015import androidx.navigation.fragment.findNavController
16+ import com.google.android.gms.location.FusedLocationProviderClient
17+ import com.google.android.gms.location.LocationServices
18+ import com.kakao.vectormap.LatLng
19+ import com.project200.common.constants.RuleConstants.SEOUL_CITY_HALL_LATITUDE
20+ import com.project200.common.constants.RuleConstants.SEOUL_CITY_HALL_LONGITUDE
21+ import com.project200.common.constants.RuleConstants.ZOOM_LEVEL
1122import com.project200.feature.matching.map.cluster.MapClusterItem
23+ import com.project200.feature.matching.map.compose.MatchingMapController
1224import com.project200.feature.matching.map.compose.MatchingMapScreen
1325import com.project200.feature.matching.map.filter.FilterBottomSheetDialog
1426import com.project200.feature.matching.map.filter.MatchingFilterRVAdapter
@@ -25,21 +37,37 @@ class MatchingMapFragment :
2537 BindingFragment <FragmentMatchingMapBinding >(R .layout.fragment_matching_map) {
2638 private val viewModel: MatchingMapViewModel by viewModels()
2739
40+ // 지도 본체(MatchingMapScreen) 내부 카메라를 제어하기 위한 통로
41+ private val mapController = MatchingMapController ()
42+ private var isMapInitialized = false
43+ private lateinit var fusedLocationClient: FusedLocationProviderClient
44+
2845 private val filterAdapter by lazy {
2946 MatchingFilterRVAdapter (
3047 onFilterClick = { type -> viewModel.onFilterTypeClicked(type) },
3148 onClearClick = { viewModel.clearFilters() },
3249 )
3350 }
3451
52+ private val locationPermissionLauncher =
53+ registerForActivityResult(ActivityResultContracts .RequestPermission ()) { isGranted ->
54+ if (isGranted) moveToCurrentLocation()
55+ }
56+
3557 override fun getViewBinding (view : View ): FragmentMatchingMapBinding {
3658 return FragmentMatchingMapBinding .bind(view)
3759 }
3860
3961 override fun setupViews () {
62+ isMapInitialized = false
63+ fusedLocationClient = LocationServices .getFusedLocationProviderClient(requireActivity())
64+
65+ // 지도 본체는 Compose(MatchingMapScreen)로 호스팅. 마커/클러스터/카메라 idle은 Screen 내부 처리.
4066 binding.mapComposeView.applyAppTheme {
4167 MatchingMapScreen (
4268 viewModel = viewModel,
69+ controller = mapController,
70+ onMapReady = { restoreInitialCamera() },
4371 onClusterClick = { items -> showMembersBottomSheet(items) },
4472 onPlaceMarkerClick = {
4573 findNavController().navigate(
@@ -56,7 +84,7 @@ class MatchingMapFragment :
5684
5785 private fun initListeners () {
5886 binding.currentLocationBtn.setOnClickListener {
59- // TODO: 현재 위치 이동 구현
87+ checkPermissionAndMove()
6088 }
6189
6290 binding.exercisePlaceListBtn.setOnClickListener {
@@ -66,6 +94,67 @@ class MatchingMapFragment :
6694 }
6795 }
6896
97+ /* *
98+ * 지도가 준비되면 1회 호출되어 초기 카메라 위치를 복원한다.
99+ * 저장된 위치가 있으면 그곳으로, 없으면 현재 위치(권한 시) 또는 기본 위치(서울시청)로 이동한다.
100+ */
101+ private fun restoreInitialCamera () {
102+ if (isMapInitialized) return
103+
104+ val savedPosition = viewModel.initialMapPosition.value
105+ if (isLocationPermissionGranted()) {
106+ if (savedPosition != null ) {
107+ mapController.moveCamera(
108+ LatLng .from(savedPosition.latitude, savedPosition.longitude),
109+ savedPosition.zoomLevel,
110+ )
111+ } else {
112+ moveToCurrentLocation()
113+ }
114+ } else {
115+ mapController.moveCamera(
116+ LatLng .from(SEOUL_CITY_HALL_LATITUDE , SEOUL_CITY_HALL_LONGITUDE ),
117+ ZOOM_LEVEL ,
118+ )
119+ locationPermissionLauncher.launch(Manifest .permission.ACCESS_FINE_LOCATION )
120+ }
121+
122+ isMapInitialized = true
123+ }
124+
125+ private fun checkPermissionAndMove () {
126+ if (isLocationPermissionGranted()) {
127+ moveToCurrentLocation()
128+ } else {
129+ locationPermissionLauncher.launch(Manifest .permission.ACCESS_FINE_LOCATION )
130+ }
131+ }
132+
133+ @SuppressLint(" MissingPermission" ) // 권한은 isLocationPermissionGranted()로 이미 확인됨
134+ private fun moveToCurrentLocation () {
135+ fusedLocationClient.lastLocation.addOnSuccessListener { location ->
136+ val latLng =
137+ if (location != null ) {
138+ LatLng .from(location.latitude, location.longitude)
139+ } else {
140+ Toast .makeText(
141+ requireContext(),
142+ R .string.error_cannot_find_current_location,
143+ Toast .LENGTH_SHORT ,
144+ ).show()
145+ LatLng .from(SEOUL_CITY_HALL_LATITUDE , SEOUL_CITY_HALL_LONGITUDE )
146+ }
147+ mapController.moveCamera(latLng, ZOOM_LEVEL )
148+ }
149+ }
150+
151+ private fun isLocationPermissionGranted (): Boolean {
152+ return ContextCompat .checkSelfPermission(
153+ requireContext(),
154+ Manifest .permission.ACCESS_FINE_LOCATION ,
155+ ) == PackageManager .PERMISSION_GRANTED
156+ }
157+
69158 override fun setupObservers () {
70159 viewLifecycleOwner.lifecycleScope.launch {
71160 repeatOnLifecycle(Lifecycle .State .STARTED ) {
0 commit comments