위치기반 출석체크 앱
구글 지도 api를 이용해서 특정 위도, 경도 100미터 이내에 있다면 출석 체크가 되는 기능의 앱을 만든다 구글 클라우드 플랫폼 콘솔에서 api 키를 발급 받고 이를 이용해서 지도에 표시한다
Geolocator 플러그인
위치 권한이 있는지 확인하고 권한을 요청한다 현재 gps위치가 바뀔 때마다 현재 위치값을 받을 수 있는 기능을 사용한다 또한 현재 위치와 설정한 위치사이 거리를 계산한다
isLocationServiceEnabled()기기 위치 서비스 기능이 켜져있는지 확인checkPermission()앱에서 위치서비스를 사용할 수 있는지 확인requestPermission()위치 권한 요청하기
Geolocator.checkPermission( ) 이렇게 사용한다
위 함수중 2,3번 함수의 반환값
| LocationPermission | 설명 |
|---|---|
| denied | 거절 상태, 한번도 요청한 적이 없을 때도 기본으로 이 상태가 반환 requestPermission()을 통해서 다시 요청할 수 있다 |
| deniedForever | 완전히 거절된 상태 이상태에서는 사용자가 설정에서 직접 허용해야한다 |
| whileInUse | 앱 사용 중 허가 상태, 앱 사용할 때만 사용 가능 |
| always | 허가상태 |
| unableToDetermine | 알 수 없음, 위치 권한을 요청할 수 없느 특정 인터넷 브라우저에서 반환 |
현재 위치를 지속적으로 반환
getPositionStream().listen()를 통해서 위치를 반환 받을 수 있다
1
2
3
Geolocator.getPositionStream().listen((Position position) {
print(position);
});
Position 클래스의 주요 속성
| 속성 | 설명 |
|---|---|
| longitude | 경도 |
| latitude | 위도 |
| timestamp | 위치가 확인된 시간, 날짜 |
| accuracy | 위치 정확도 |
| speed | 이동 속도 |
| speedAccuracy | 이동 속도 정확도 |
두 위치 사이 거리 구하기
1
2
3
4
5
6
final distance = Geolocator.distanceBetween(
sLat, // 시작 위도
sLng, // 시작 경도
eLat, // 끝 위도
eLng, // 끝 경도
);
구글 지도 api 키 발급
gcp에서 프로젝트를 생성하고 해당 프로젝트의 API 및 서비스 -> 라이브러리 클릭한다
Maps SDK for Android와 Maps SDK for iOS를 사용한다
키 및 사용자 인증 정보 -> 사용자 인증 정보 만들기 -> API 키를 누른다 이후 발급 받은 키를 저장한다
프로젝트
1. pubspec.yaml 설정하기
구글맵 플러그인과 geolocator 플러그인을 추가한다
1
2
3
4
5
6
7
dependencies:
flutter:
sdk: flutter
google_maps_flutter: ^2.10.0
geolocator: ^13.0.2
cupertino_icons: ^1.0.8
ios실행 버전 오류시 (iphone 16 pro) ios/Podfile여기서
# platform :ios, '12.0' 이걸 주석 해제하고 14.0으로 변경한다
안드로이드 실행 오류 Execution failed for task ':flutter_plugin_android_lifecycle:compileDebugJavaWithJavac'.
위 오류를 보고 android/setting.gradle에 들어가서
1
2
3
4
5
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.2.1" apply false
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
}
8.1.0 -> 8.2.1로 수정 후 정삭적으로 실행되었다
2. 네이티브 코드 설정하기
안드로이드 설정하기 위해서는 android/app/src/main/AndroidManifest.xml에서 권한과 api키를 등록한다
1
2
3
4
5
6
7
8
9
10
11
12
13
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!--상세 위치 권한 등록-->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<application
android:label="mapapp"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity>
</activity>
<!-- 구글 api 등록 -->
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="키입력" />
iOS 설정하기 ios/Runner/AppDelegate.swift 수정해야 한다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import Flutter
import UIKit
import GoogleMaps // Google Maps SDK를 추가합니다.
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// Google Maps API 키 등록
GMSServices.provideAPIKey("키 입력")
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
iOS 권한 요청하기 ios/Runner/info.plist를 수정
1
2
3
4
5
6
<dict>
<key>NSLocationWhenInUseUsageDescription</key>
<string>위치 정보가 필요합니다.</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>위치 정보가 필요합니다.</string>
</dict>
위 코드를 추가한다
2. 프로젝트
프로젝트는 앱바, 바디, 풋터 이렇게 3가지로 구성된다 바디에는 구글 지도를 보여주고 정확한 위치에 마커를 띄우고 도형을 사용해서 출첵 가능한 영역을 표시해준다 풋터는 출첵 기능을 구현할 영역이다 Gps를 통해서 출첵 가능한 위치라면 버튼을 띄우고 해당 위치가 아니라면 버튼을 보여주지 않는다
1. 앱바 구현
앱바는 기본적으로 제공되는 위젯으로 만들 수 있다
renderAppBar() 위젯을 만들어서 앱바 기능을 구현한다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// HomeScreen.dart
import 'package:flutter/material.dart';
class HomeScreen extends StatelessWidget {
const HomeScreen({Key? key}) : super(key : key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: renderAppBar(),
body: Text("home"),
);
}
AppBar renderAppBar() {
return AppBar(
centerTitle: true,
title: Text('오늘도 출첵',
style: TextStyle(
color: Colors.blue,
fontWeight: FontWeight.w700,
),
),
backgroundColor: Colors.white,
);
}
}
2. 바디 구현
구글 지도를 body에 띄운다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
class HomeScreen extends StatelessWidget {
static final LatLng companyLatLng = LatLng(
37.5233273,
126.921252,
);
const HomeScreen({Key? key}) : super(key : key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: renderAppBar(),
body: GoogleMap(
initialCameraPosition: CameraPosition(
target: companyLatLng,
zoom: 16,
),
),
);
}
}
초기 위치를 설정하고 구글 맵 위젯에 초기 위치, 확대 비율 이렇게 매개변수를 넣으면 지도 출력이 간단하게 가능하다
3. 풋터 구현
지금은 body가 아래 부분의 전체를 차지하고 있기 때문에 Column위젯 내부에 body와 footer를 넣어 2:1로 만든다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
class HomeScreen extends StatelessWidget {
static final LatLng companyLatLng = LatLng(
37.5233273,
126.921252,
);
const HomeScreen({Key? key}) : super(key : key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: renderAppBar(),
body: Column(children: [
Expanded(//비율 맞추기
flex: 2,
child: GoogleMap(
initialCameraPosition: CameraPosition(
target: companyLatLng,
zoom: 16,
),
),
),
Expanded( //비율 맞추기
flex: 1,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.timelapse_outlined,
color: Colors.blue,
size: 50,
),
const SizedBox(height: 20.0,),
ElevatedButton(
onPressed: () {},
child: Text('출첵하기'))
],
),
),
],
)
);
}
}
4. 위치 권한
기기 자체의 gps 사용 권한을 확인하고 위치 권한을 사용할 수 없는 상태면 권한을 재요청하는 로직을 구현하고 위젯을 보여줄때 위치권한 데이터를 받을 수 있을때와 없을 때를 구분해서 코드를 작성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:geolocator/geolocator.dart';
class HomeScreen extends StatelessWidget {
static final LatLng companyLatLng = LatLng(
37.5233273,
126.921252,
);
const HomeScreen({Key? key}) : super(key : key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: renderAppBar(),
body: FutureBuilder<String>(
future: checkPermission(),
builder: (context, snapshot) {
if(!snapshot.hasData &&
snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator(),
);
}
if(snapshot.data == '위치 권한이 허가 되었습니다.'){
return Column(
children: [
Expanded(
flex: 2,
child: GoogleMap(
initialCameraPosition: CameraPosition(
target: companyLatLng,
zoom: 16,
),
),
),
Expanded(
flex: 1,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.timelapse_outlined,
color: Colors.blue,
size: 50,
),
const SizedBox(height: 20.0,),
ElevatedButton(
onPressed: () {},
child: Text('출첵하기')
),
],
),
),
],
);
}
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 isLocationEnabled = await Geolocator.isLocationServiceEnabled();
// 활성화 여부 체크
if(!isLocationEnabled) {
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 '위치 권한이 허가 되었습니다.';
}
}
5. 화면에 마커 그리기
마커는 하나의 상수에 위도와 경도를 넣고 해당 위도, 경도 데이터를 다시 Marker 자료형에 넣어서 id와 위치를 넣어준다 그후 GoogleMap 매개변수에 markers에 넣어주면 마커를 표시할 수 있다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class HomeScreen extends StatelessWidget {
//생략
static final LatLng schoolLatLng = LatLng(
36.627961,
127.456472,
);
static final Marker marker = Marker(
markerId: MarkerId('school'),
position: schoolLatLng,
);
//생략
Expanded(
flex: 2,
child: GoogleMap(
initialCameraPosition: CameraPosition(
target: companyLatLng,
zoom: 16,
),
markers: Set.from([marker]), // 마커를 추가한다
),
),
6. 마커 반경 표시
마커의 반경을 표시하기 위해서는 특정 지점에 추가적으로 circle을 만들면 된다 Circle에 id값을 넣어주지 않으면 중복처리가 되어서 Set에서 배제될 수 있다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static final Circle circle = Circle(
circleId: CircleId('choolCheckCircle'),
center: schoolLatLng,
fillColor: Colors.blue.withOpacity(0.5),
radius: 100, //원의 반지를
strokeColor: Colors.blue, // 원의 테두리 색
strokeWidth: 1, //원의 테두리 두께
);
Expanded(
flex: 2,
child: GoogleMap(
initialCameraPosition: CameraPosition(
target: companyLatLng,
zoom: 16,
),
markers: Set.from([marker]),
circles: Set.from([circle]),//이 부분 추가
),
),
7. 현재위치 지도에 표시하기
GoogleMap의 위젯에 매개변수에 myLocationEnabled: true,를 추가하면 현재 위치가 파란 점으로 표시되고 버튼을 눌러 현재위치로 화면 이동이 가능하다
8. 기능 구현
버튼을 눌러서 출첵 기능을 구현한다 출첵할 수 없는 위치라면 출첵할 수 없는 위치입니다라는 메세지와 함께 취소 버튼을 보여준다 현재 위치를 받고 목적지 사이 거리를 계산하기 위해서 Geolocator.getCurrentPosition()함수를 통해서 현재 위치를 받고 Geolocator.distanceBetween()을 이용해서 거리를 구할 수 있다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:geolocator/geolocator.dart';
class HomeScreen extends StatelessWidget {
static final LatLng schoolLatLng = LatLng(
36.627961,
127.45660,
);
static final Marker marker = Marker(
markerId: MarkerId('school'),
position: schoolLatLng,
);
static final Circle circle = Circle(
circleId: CircleId('choolCheckCircle'),
center: schoolLatLng,
fillColor: Colors.blue.withOpacity(0.3),
radius: 100, //원의 반지를
strokeColor: Colors.blue, // 원의 테두리 색
strokeWidth: 1, //원의 테두리 두께
);
const HomeScreen({Key? key}) : super(key : key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: renderAppBar(),
body: FutureBuilder<String>(
future: checkPermission(),
builder: (context, snapshot) {
if(!snapshot.hasData &&
snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator(),
);
}
if(snapshot.data == '위치 권한이 허가 되었습니다.'){
return Column(
children: [
Expanded(
flex: 2,
child: GoogleMap(
initialCameraPosition: CameraPosition(
target: schoolLatLng,
zoom: 16,
),
myLocationEnabled: true,
markers: Set.from([marker]),
circles: Set.from([circle]),
),
),
Expanded(
flex: 1,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.timelapse_outlined,
color: Colors.blue,
size: 50,
),
const SizedBox(height: 20.0,),
ElevatedButton(
onPressed: () async {
final curPosition = await Geolocator.getCurrentPosition(); // 현재 위치를 가져옴
final distance = Geolocator.distanceBetween(
curPosition.latitude,
curPosition.longitude,
schoolLatLng.latitude,
schoolLatLng.longitude
); // 사이 거리 구하기
bool canCheck = distance < 100; //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('출첵하기'),
)
]
);
}
);
},
child: Text('출첵하기')
),
],
),
),
],
);
}
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 isLocationEnabled = await Geolocator.isLocationServiceEnabled();
// 활성화 여부 체크
if(!isLocationEnabled) {
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 '위치 권한이 허가 되었습니다.';
}
}