Skip to content

Commit 2d32884

Browse files
edv-Shinclaude
andcommitted
refactor: MatchingMapController로 카메라 제어 + 현재위치/초기복원 복원 (#546)
- MatchingMapController 추가: Screen 내부 MapViewManager에 다리를 놓아 화면 밖(Fragment)에서 moveCamera/currentBounds 호출 - MatchingMapScreen: controller/onMapReady 파라미터 추가, 지도 준비 시 통로 연결 - MatchingMapFragment: 현재위치 버튼·초기 카메라 복원을 controller 경유로 복원 (권한/FusedLocation은 Fragment 책임 유지) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 17157c0 commit 2d32884

3 files changed

Lines changed: 125 additions & 3 deletions

File tree

feature/matching/src/main/java/com/project200/feature/matching/map/MatchingMapFragment.kt

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,26 @@
11
package com.project200.feature.matching.map
22

3+
import android.Manifest
4+
import android.annotation.SuppressLint
5+
import android.content.pm.PackageManager
36
import android.view.View
47
import android.widget.Toast
8+
import androidx.activity.result.contract.ActivityResultContracts
9+
import androidx.core.content.ContextCompat
510
import androidx.core.view.isVisible
611
import androidx.fragment.app.viewModels
712
import androidx.lifecycle.Lifecycle
813
import androidx.lifecycle.lifecycleScope
914
import androidx.lifecycle.repeatOnLifecycle
1015
import 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
1122
import com.project200.feature.matching.map.cluster.MapClusterItem
23+
import com.project200.feature.matching.map.compose.MatchingMapController
1224
import com.project200.feature.matching.map.compose.MatchingMapScreen
1325
import com.project200.feature.matching.map.filter.FilterBottomSheetDialog
1426
import 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) {
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.project200.feature.matching.map.compose
2+
3+
import com.kakao.vectormap.LatLng
4+
import com.project200.domain.model.MapBounds
5+
import com.project200.feature.matching.map.MapViewManager
6+
7+
/**
8+
* 지도 카메라를 화면 밖(Fragment)에서 제어하기 위한 통로.
9+
*
10+
* MapViewManager 는 MatchingMapScreen 내부에서 비동기로 생성되므로,
11+
* 권한/현재위치/초기 복원처럼 Fragment 가 담당하는 로직이 카메라를 만지려면 이 컨트롤러로 다리를 놓는다.
12+
* 준비 전(manager == null)에는 카메라 명령이 무시된다.
13+
*/
14+
class MatchingMapController {
15+
internal var manager: MapViewManager? = null
16+
17+
val isReady: Boolean
18+
get() = manager != null
19+
20+
fun moveCamera(
21+
latLng: LatLng,
22+
zoomLevel: Int,
23+
) {
24+
manager?.moveCamera(latLng, zoomLevel)
25+
}
26+
27+
fun currentBounds(): MapBounds? = manager?.getCurrentBounds()
28+
}

feature/matching/src/main/java/com/project200/feature/matching/map/compose/MatchingMapScreen.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import com.project200.feature.matching.map.cluster.MapClusterItem
3030
fun MatchingMapScreen(
3131
viewModel: MatchingMapViewModel,
3232
modifier: Modifier = Modifier,
33+
controller: MatchingMapController? = null,
34+
onMapReady: () -> Unit = {},
3335
onClusterClick: (List<MapClusterItem>) -> Unit = {},
3436
onPlaceMarkerClick: () -> Unit = {},
3537
) {
@@ -67,10 +69,13 @@ fun MatchingMapScreen(
6769
onLabelClick = { label ->
6870
handleLabelClick(label, manager, clusterCalculator, onClusterClick, onPlaceMarkerClick)
6971
},
70-
)
72+
).also { mgr ->
73+
controller?.manager = mgr // 카메라 제어 통로 연결
74+
onMapReady() // Fragment가 초기 복원 등을 시작하도록 통지
75+
}
7176
},
7277
)
73-
// TODO: 필터, 현재위치 버튼 등 오버레이 여기에 추가
78+
// 현재위치 버튼 등 오버레이는 Fragment XML 유지(추후 Compose 전환)
7479
}
7580

7681
// 데이터(또는 지도 준비) 변경 시 마커/클러스터 다시 그리기

0 commit comments

Comments
 (0)