13장 오늘도 출첵

송기영·2023년 12월 25일
0

플러터

목록 보기
15/25

특정위치의 위도, 경도부터 100미터 이내에서 출근 체크 기능을 제공하는 앱으로 구글 지도 API를 이용한다.

13.1 사전지식

13.1.1 Geolocator 플러그인

지리와 관련된 기능을 쉽게 사용할 수 있는 플러그인으로 위치 서비스 사용 권한을 확인하고 요청, GPS 위치가 바뀔 때마다 현재 위칫값을 받을 수 있는 기능, 회사 건물 간의 거리를 계산하는 기능을 가진다.

LocationPermission설명
denied거절상태, requestPermission() 함수를 이용해서 권한 요청가능
deniedForever완전히 거절된 상태, 다시 권한 요청이 불가능
whileInUse앱 사용중 허가상태
always허가 상태
unableToDetermine알 수 없음

현재 위치 지속적으로 반환받기

getPositionStream() 함수를 이용하면 현재 위치가 변경될 때마다 주기적으로 반환받을 수 있다.

Geolocator.getPositionStream().listen((Position position) {
	print(position);
});

position 속성

속성설명
longitude경도
latitude위도
timestamp위치가 확인된 날짜 및 시간
accuracy위치 정확도, 특정기기에서는 확인이 불가능
speed이동 속도, 특정기기에서는 확인 불가능
speedAccuracy이동 속도 정확도, 특정기기에서 확인 불가능

두 위치 간의 거리 구하기

final distance = Geolocator.distanceBetween(
	sLat,  // 시작점 위도
	sLng,  // 시작점 경도
	eLat,  // 끝지점 위도
	eLng,  // 끝지점 경도
);

13.2 준비하기

13.2.1 구글 지도 API 키 발급받기

  1. 구글에서 구글 클라우드 플랫폼 검색 혹은 링크로 이동해 구글 클라우드 플랫폼에 접속
  2. 회원가입 및 로그인을 진행
  3. 콘솔을 클릭 후 프로젝트 선택에서 새 프로젝트 클릭
  4. flutter-google-map-project 이름의 프로젝트를 생성
  5. 왼쪽 메뉴를 클릭 후 제품 더 보기 → Google Maps Platform을 찾아 클릭
  6. API 및 서비스 클릭 후 결제정보를 입력하여 API를 활성화
  7. 왼쪽 메뉴 키 및 사용자 인증 정보 클릭 후 만들어진 Maps AI Key를 발급

13.2.2 google_maps_flutter 플러그인 설치

pubsepc.yaml에 google_maps_flutter을 입력해 설치해준다.

dependencies:
  cupertino_icons: ^1.0.6
  flutter:
    sdk: flutter
  google_maps_flutter: ^2.5.0

13.2.3 네이티브 코드 설정

구글 지도를 사용하려면 안드로이드와 IOS 모두 네이티브 설정이 필요하다.

안드로이드

android/app/build.gradle파일에서 compileSdk 33, minSdkVersion 20으로 설정해준다.

android {
    namespace "com.example.chool_check"
    compileSdk 33
    ndkVersion flutter.ndkVersion

    defaultConfig {       
        applicationId "com.example.chool_check"
        minSdkVersion 20
        targetSdkVersion flutter.targetSdkVersion
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
    }

}

android/app/src/main/AndroidManifest.xml 파일에서 상세 위치 권한과 구글지도 API 키를 등록한다.

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <application
        android:label="chool_check"
        android:name="${applicationName}"
        android:icon="@mipmap/ic_launcher"
        >
        <meta-data android:name="com.google.android.geo.API_KEY" android:value="발급된 API 키 입력" />
</manifest>

IOS

ios/Runner/AppDelegate.seift파일에 아래코드를 덮어쓴다.

import UIKit
import Flutter
import GoogleMaps


 class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GMSServices.provideAPIKey("발근된 API 키 입력")
    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}

ios/Runner/Info.plist파일을 열고 아래 내용을 추가해준다.

<dict>
	<key>NSLocationWhenInUseUsageDescription</key>
	<string>위치 정보가 필요합니다.</string>
	<key>NSLocationAlwaysUsageDescription</key>
	<string>위치 정보가 필요합니다.</string>
</dict>

13.3 구현하기

// lib/screen/home_screen.dart

import "package:flutter/material.dart";
import "package:geolocator/geolocator.dart";
import "package:google_maps_flutter/google_maps_flutter.dart";

class HomeScreen extends StatelessWidget {
  // 지도 초기화 위치
  static final LatLng companyLatLen = LatLng(37.2933, 127.0705);
  static final Marker marker =
      Marker(markerId: MarkerId("company"), position: companyLatLen);
  static final Circle circle = Circle(
    circleId: CircleId("choolCheckCircle"),
    center: companyLatLen,
    fillColor: Colors.blue.withOpacity(0.5),
    radius: 100,
    strokeColor: Colors.blue,
    strokeWidth: 1,
  );
  const HomeScreen({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: renderAppBar(),
      body: FutureBuilder<String>(
        future: checkPermission(),
        builder: (context, snapshot) {
          if (!snapshot.hasData &&
              snapshot.connectionState == ConnectionState.waiting) {
            return const Center(
              child: CircularProgressIndicator(),
            );
          }

          if (snapshot.data == "위치 권한이 허가 되었습니다.") {
            return (Column(children: [
              Expanded(
                  flex: 2,
                  child: GoogleMap(
                    initialCameraPosition:
                        CameraPosition(target: companyLatLen, zoom: 16),
                    // 내 위치 지도에 보여주기
                    myLocationEnabled: true,
                    // 마커표시
                    markers: Set.from([marker]),
                    circles: Set.from([circle]),
                  )),
              Expanded(
                  child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  const Icon(
                    Icons.timelapse_outlined,
                    color: Colors.blue,
                    size: 50.0,
                  ),
                  const SizedBox(
                    height: 20.0,
                  ),
                  ElevatedButton(
                      onPressed: () async {
                        final curPosition =
                            await Geolocator.getCurrentPosition();
                        final distance = Geolocator.distanceBetween(
                            curPosition.latitude,
                            curPosition.longitude,
                            companyLatLen.latitude,
                            companyLatLen.longitude);

                        bool canCheck = distance < 100;
                        showDialog(
                            context: context,
                            builder: (_) {
                              return AlertDialog(
                                title: Text("출근하기"),
                                content: Text(canCheck
                                    ? "출근을 하시겠습니까?"
                                    : "출근할 수 없는 위치입니다."),
                                actions: [
                                  TextButton(
                                      onPressed: () {
                                        Navigator.of(context).pop(false);
                                      },
                                      child: Text("취소")),
                                  if (canCheck)
                                    TextButton(
                                        onPressed: () {
                                          Navigator.of(context).pop(true);
                                        },
                                        child: Text("출근하기"))
                                ],
                              );
                            });
                      },
                      style: ElevatedButton.styleFrom(
                          backgroundColor: Colors.blue,
                          shape: RoundedRectangleBorder(
                              borderRadius: BorderRadius.circular(4.0))),
                      child: const Text(
                        "출근하기!",
                        style: TextStyle(color: Colors.white),
                      ))
                ],
              ))
            ]));
          }

          return Center(
            child: Text(snapshot.data.toString()),
          );
        },
      ),
    );
  }

  AppBar renderAppBar() {
    return AppBar(
      centerTitle: true,
      title: Text("오늘도 출첵",
          style: TextStyle(color: Colors.blue, fontWeight: FontWeight.w700)),
      backgroundColor: Colors.white,
    );
  }

  Future<String> checkPermission() async {
    // 위치 서비스 활성화 여부 확인
    final isLocationEnalbed = await Geolocator.isLocationServiceEnabled();

    if (!isLocationEnalbed) {
      return "위치 서비스를 활성화해주세요.";
    }

    // 위치권한 확인
    LocationPermission checkPermission = await Geolocator.checkPermission();

    if (checkPermission == LocationPermission.denied) {
      checkPermission = await Geolocator.requestPermission();
      if (checkPermission == LocationPermission.denied) {
        return "위치 권한을 허가해주세요.";
      }
    }

    if (checkPermission == LocationPermission.deniedForever) {
      return "앱의 위치 권한을 설정에서 허가해주세요.";
    }

    return "위치 권한이 허가 되었습니다.";
  }
}
profile
업무하면서 쌓인 노하우를 정리하는 블로그🚀 풀스택 개발자를 지향하고 있습니다👻

0개의 댓글