Today's

길을 나서지 않으면 그 길에서 만날 수 있는 사람을 만날 수 없다

모바일 앱(안드로이드)

외국인 관광객을 위한 한국 여행 가이드 앱 개발 일지 - 위치 권한과 구글맵 화면 구현 (Jetpack Compose + Hilt)

Billcorea 2025. 5. 28. 15:10
반응형

외국인을 위한 한국 여행 가이드 앱 개발 일지 - 위치 권한과 구글맵 화면 구현 (Jetpack Compose + Hilt)

앱 화면

 

오늘은 Jetpack Compose 기반으로 개발 중인 한국 여행 가이드 앱에서
인트로 화면 이후 위치 권한을 요청하고, 구글 지도를 표시하는 메인 화면을 구현했습니다.


✅ 오늘 구현한 핵심 기능

항목 구현 방식
위치 권한 요청 Accompanist Permissions
위치 정보 획득 FusedLocationProviderClient (Hilt 주입)
지도 표시 Google Maps Compose
기본 위치 fallback 서울 시청 (37.5665, 126.9780)
권한 거부 시 안내 설정 화면으로 유도 (Intent)
상단바 겹침 방지 statusBarsPadding() 적용

📦 Hilt로 FusedLocationProviderClient 주입

@Module
@InstallIn(SingletonComponent::class)
object LocationModule {

    @Provides
    fun provideFusedLocationProviderClient(
        @ApplicationContext context: Context
    ): FusedLocationProviderClient =
        LocationServices.getFusedLocationProviderClient(context)
}

🧭 ViewModel에서 현재 위치 가져오기

@HiltViewModel
class MapViewModel @Inject constructor(
    private val locationClient: FusedLocationProviderClient
) : ViewModel() {

    var currentLocation by mutableStateOf<LatLng?>(null)
        private set

    fun fetchLocation(context: Context) {
        try {
            locationClient.lastLocation.addOnSuccessListener { location ->
                location?.let {
                    currentLocation = LatLng(it.latitude, it.longitude)
                }
            }
        } catch (e: SecurityException) {
            e.printStackTrace()
        }
    }
}

🗺️ MapScreen Composable

@Composable
fun MapScreen(viewModel: MapViewModel = hiltViewModel()) {
    val context = LocalContext.current
    val permissionState = rememberMultiplePermissionsState(
        listOf(
            Manifest.permission.ACCESS_FINE_LOCATION,
            Manifest.permission.ACCESS_COARSE_LOCATION
        )
    )

    val defaultLocation = LatLng(37.5665, 126.9780)
    val currentLocation = viewModel.currentLocation ?: defaultLocation
    val cameraPositionState = rememberCameraPositionState {
        position = CameraPosition.fromLatLngZoom(currentLocation, 15f)
    }

    LaunchedEffect(Unit) {
        permissionState.launchMultiplePermissionRequest()
    }

    LaunchedEffect(permissionState.allPermissionsGranted) {
        if (permissionState.allPermissionsGranted) {
            viewModel.fetchLocation(context)
        }
    }

    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text("한국 여행 가이드") },
                modifier = Modifier.statusBarsPadding()
            )
        },
        contentWindowInsets = WindowInsets.systemBars
    ) { paddingValues ->
        Box(
            modifier = Modifier
                .fillMaxSize()
                .padding(paddingValues)
        ) {
            when {
                permissionState.allPermissionsGranted -> {
                    GoogleMap(
                        modifier = Modifier.fillMaxSize(),
                        cameraPositionState = cameraPositionState
                    ) {
                        Marker(
                            state = MarkerState(position = currentLocation),
                            title = "현재 위치"
                        )
                    }
                }

                permissionState.shouldShowRationale -> {
                    PermissionExplanationUI {
                        permissionState.launchMultiplePermissionRequest()
                    }
                }

                else -> {
                    PermissionDeniedUI()
                }
            }
        }
    }
}

👮 권한 거절 대응 UI

@Composable
fun PermissionExplanationUI(onRequest: () -> Unit) {
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("이 기능을 사용하려면 위치 권한이 필요합니다.")
        Button(onClick = onRequest) {
            Text("권한 요청하기")
        }
    }
}

@Composable
fun PermissionDeniedUI() {
    val context = LocalContext.current
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("위치 권한이 영구적으로 거부되었습니다.")
        Button(onClick = {
            context.startActivity(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
                data = Uri.fromParts("package", context.packageName, null)
            })
        }) {
            Text("설정에서 권한 허용하기")
        }
    }
}

📌 상단바가 StatusBar와 겹칠 때 해결법

TopAppBar에 Modifier.statusBarsPadding()을 추가하면 겹침 현상이 해결됩니다.

TopAppBar(
    title = { Text("앱 이름") },
    modifier = Modifier.statusBarsPadding()
)

또한 Scaffold에 아래 설정을 함께 추가하는 것이 좋습니다:

contentWindowInsets = WindowInsets.systemBars

📘 다음 목표

  • 실시간 위치 추적 추가
  • 주변 관광지 마커 표시
  • Navigation Compose와 통합

이 앱은 Jetpack Compose, Hilt, Room 등을 활용해 외국인들이 한국 여행을 쉽게 즐길 수 있도록 돕는 것이 목적입니다.
계속해서 개발 과정을 공유해보겠습니다! 👋

반응형