Flutter – 근퇴관리 앱 (토이 프로젝트)

  • 주요 스택
    • Google Maps 지도
    • 지도 마커
    • 지도 동그라미 표시
    • 현재 위치 표시 및 위도/경도 구하기
    • 위도, 경도간 거리 구하기

지도 띄워보기

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
// latitude 위도 , longitude 경도
static final LatLng companyLatLng = LatLng(10.800715, 106.7120223);
static final CameraPosition initialPosition = CameraPosition(
target: companyLatLng, // 최초 포지션
zoom: 15, // zoom level
);
@override
Widget build(BuildContext context) {
return Scaffold(
body: GoogleMap(
initialCameraPosition: initialPosition,
),
);
}
}
import 'package:flutter/material.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @override State<HomeScreen> createState() => _HomeScreenState(); } class _HomeScreenState extends State<HomeScreen> { // latitude 위도 , longitude 경도 static final LatLng companyLatLng = LatLng(10.800715, 106.7120223); static final CameraPosition initialPosition = CameraPosition( target: companyLatLng, // 최초 포지션 zoom: 15, // zoom level ); @override Widget build(BuildContext context) { return Scaffold( body: GoogleMap( initialCameraPosition: initialPosition, ), ); } }
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';

class HomeScreen extends StatefulWidget {
  const HomeScreen({super.key});

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  // latitude 위도 , longitude 경도
  static final LatLng companyLatLng = LatLng(10.800715, 106.7120223);

  static final CameraPosition initialPosition = CameraPosition(
    target: companyLatLng, // 최초 포지션
    zoom: 15, // zoom level
  );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: GoogleMap(
        initialCameraPosition: initialPosition,
      ),
    );
  }
}

AppBar 추가, 상 하단 분리

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
// latitude 위도 , longitude 경도
static final LatLng companyLatLng = LatLng(10.800715, 106.7120223);
static final CameraPosition initialPosition = CameraPosition(
target: companyLatLng, // 최초 포지션
zoom: 15, // zoom level
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: renderAppBar(),
body: Column(
children: [
_CustomGoogleMap(initialPosition: initialPosition),
_ChoolCheckButton(),
],
),
);
}
AppBar renderAppBar() {
return AppBar(
title: Text(
'오늘도 출근',
style: TextStyle(
color: Colors.blue,
fontWeight: FontWeight.w700,
),
),
backgroundColor: Colors.white,
);
}
}
class _CustomGoogleMap extends StatelessWidget {
final CameraPosition initialPosition;
const _CustomGoogleMap({
required this.initialPosition,
super.key,
});
@override
Widget build(BuildContext context) {
return Expanded(
flex: 2,
child: GoogleMap(
mapType: MapType.normal,
// hybrid - 위성 지도 , normal - 일반 지도, terrain - 고저 지도 등
initialCameraPosition: initialPosition,
),
);
}
}
class _ChoolCheckButton extends StatelessWidget {
const _ChoolCheckButton({super.key});
@override
Widget build(BuildContext context) {
return Expanded(
child: Text(
'출근',
),
);
}
}
import 'package:flutter/material.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @override State<HomeScreen> createState() => _HomeScreenState(); } class _HomeScreenState extends State<HomeScreen> { // latitude 위도 , longitude 경도 static final LatLng companyLatLng = LatLng(10.800715, 106.7120223); static final CameraPosition initialPosition = CameraPosition( target: companyLatLng, // 최초 포지션 zoom: 15, // zoom level ); @override Widget build(BuildContext context) { return Scaffold( appBar: renderAppBar(), body: Column( children: [ _CustomGoogleMap(initialPosition: initialPosition), _ChoolCheckButton(), ], ), ); } AppBar renderAppBar() { return AppBar( title: Text( '오늘도 출근', style: TextStyle( color: Colors.blue, fontWeight: FontWeight.w700, ), ), backgroundColor: Colors.white, ); } } class _CustomGoogleMap extends StatelessWidget { final CameraPosition initialPosition; const _CustomGoogleMap({ required this.initialPosition, super.key, }); @override Widget build(BuildContext context) { return Expanded( flex: 2, child: GoogleMap( mapType: MapType.normal, // hybrid - 위성 지도 , normal - 일반 지도, terrain - 고저 지도 등 initialCameraPosition: initialPosition, ), ); } } class _ChoolCheckButton extends StatelessWidget { const _ChoolCheckButton({super.key}); @override Widget build(BuildContext context) { return Expanded( child: Text( '출근', ), ); } }
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';

class HomeScreen extends StatefulWidget {
  const HomeScreen({super.key});

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  // latitude 위도 , longitude 경도
  static final LatLng companyLatLng = LatLng(10.800715, 106.7120223);

  static final CameraPosition initialPosition = CameraPosition(
    target: companyLatLng, // 최초 포지션
    zoom: 15, // zoom level
  );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: renderAppBar(),
      body: Column(
        children: [
          _CustomGoogleMap(initialPosition: initialPosition),
          _ChoolCheckButton(),
        ],
      ),
    );
  }

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

class _CustomGoogleMap extends StatelessWidget {
  final CameraPosition initialPosition;

  const _CustomGoogleMap({
    required this.initialPosition,
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    return Expanded(
      flex: 2,
      child: GoogleMap(
        mapType: MapType.normal,
        // hybrid - 위성 지도 , normal - 일반 지도, terrain - 고저 지도 등
        initialCameraPosition: initialPosition,
      ),
    );
  }
}

class _ChoolCheckButton extends StatelessWidget {
  const _ChoolCheckButton({super.key});

  @override
  Widget build(BuildContext context) {
    return Expanded(
      child: Text(
        '출근',
      ),
    );
  }
}

Geolocator 앱 권한 체크 및 허용 요청

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
Future<String> checkPermission() async {
// 권한 관련 작업은 모두 async로 한다. 유저가 누를때까지 기다려야하기 때문
final isLocationEnabled = await Geolocator
.isLocationServiceEnabled(); // 해당 기기가 위치 서비스를 사용중인지 확인 (앱이 아닌 디바이스 자체의 위치서비스)
if(!isLocationEnabled) {
return '위치 서비스를 활성화 해주세요.';
}
/*
* LocationPermission 타입
* denied : default >> Geolocator.requestPermission() 로 권한 요청 하면됨
* deniedForever : 권한을 허가하지 않은 것 >> 다시 앱이 권한을 요청할 수 없음 (유저가 직접 설정으로 가서 권한을 열도록 유도해야함)
* whileInUse : 앱 실행중에만 권한 허가
* always : 항상 허가
* */
LocationPermission checkedPermission = await Geolocator.checkPermission(); // 앱의 위치서비스 권한 확인
if(checkedPermission == LocationPermission.denied) {
checkedPermission = await Geolocator.requestPermission();
if(checkedPermission == LocationPermission.denied) {
return '위치 권한을 허가해주세요.';
}
}
if(checkedPermission == LocationPermission.deniedForever){
return '앱의 위치 권한을 세팅에서 허가해주세요.';
}
return '위치 권한이 허가되었습니다.';
}
Future<String> checkPermission() async { // 권한 관련 작업은 모두 async로 한다. 유저가 누를때까지 기다려야하기 때문 final isLocationEnabled = await Geolocator .isLocationServiceEnabled(); // 해당 기기가 위치 서비스를 사용중인지 확인 (앱이 아닌 디바이스 자체의 위치서비스) if(!isLocationEnabled) { return '위치 서비스를 활성화 해주세요.'; } /* * LocationPermission 타입 * denied : default >> Geolocator.requestPermission() 로 권한 요청 하면됨 * deniedForever : 권한을 허가하지 않은 것 >> 다시 앱이 권한을 요청할 수 없음 (유저가 직접 설정으로 가서 권한을 열도록 유도해야함) * whileInUse : 앱 실행중에만 권한 허가 * always : 항상 허가 * */ LocationPermission checkedPermission = await Geolocator.checkPermission(); // 앱의 위치서비스 권한 확인 if(checkedPermission == LocationPermission.denied) { checkedPermission = await Geolocator.requestPermission(); if(checkedPermission == LocationPermission.denied) { return '위치 권한을 허가해주세요.'; } } if(checkedPermission == LocationPermission.deniedForever){ return '앱의 위치 권한을 세팅에서 허가해주세요.'; } return '위치 권한이 허가되었습니다.'; }
Future<String> checkPermission() async {
    // 권한 관련 작업은 모두 async로 한다. 유저가 누를때까지 기다려야하기 때문
    final isLocationEnabled = await Geolocator
        .isLocationServiceEnabled(); // 해당 기기가 위치 서비스를 사용중인지 확인 (앱이 아닌 디바이스 자체의 위치서비스)

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

    /*
    * LocationPermission 타입
    * denied : default >> Geolocator.requestPermission() 로 권한 요청 하면됨
    * deniedForever : 권한을 허가하지 않은 것 >> 다시 앱이 권한을 요청할 수 없음 (유저가 직접 설정으로 가서 권한을 열도록 유도해야함)
    * whileInUse : 앱 실행중에만 권한 허가
    * always : 항상 허가
    * */

    LocationPermission checkedPermission = await Geolocator.checkPermission(); // 앱의 위치서비스 권한 확인
    if(checkedPermission == LocationPermission.denied) {
      checkedPermission = await Geolocator.requestPermission();

      if(checkedPermission == LocationPermission.denied) {
        return '위치 권한을 허가해주세요.';
      }
    }

    if(checkedPermission == LocationPermission.deniedForever){
      return '앱의 위치 권한을 세팅에서 허가해주세요.';
    }

    return '위치 권한이 허가되었습니다.';
  }

FutureBuilder 를 통한 Future 상태 관리

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
// latitude 위도 , longitude 경도
static final LatLng companyLatLng = LatLng(10.800715, 106.7120223);
static final CameraPosition initialPosition = CameraPosition(
target: companyLatLng, // 최초 포지션
zoom: 15, // zoom level
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: renderAppBar(),
body: FutureBuilder( // FutureBuilder를 사용하여 리턴되는 값과 Future를 가져오는 함수의 상태에 따라 다른 UI를 렌더링 할수 있다.
future: checkPermission(), // Future 를 리턴해주는 아무 함수나 넣을 수 있다.
builder: (BuildContext context, AsyncSnapshot snapshot) {
// future 에 넣은 함수의 리턴값은 snapshot에 들어간다.
print(snapshot.connectionState); // none, waiting, active, done (future 함수의 진행상태)
print(snapshot.data); // '위치 권한이 허가되었습니다'
if (snapshot.connectionState == ConnectionState.waiting) {
// 아직 사용자의 응답을 기다리고 있을때는 로딩바
return Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.data == '위치 권한이 허가되었습니다.') {
return Column(
children: [
_CustomGoogleMap(
initialPosition: initialPosition,
),
_ChoolCheckButton(),
],
);
}
return Center(
child: Text(snapshot.data),
);
},
),
);
}
Future<String> checkPermission() async {
// 권한 관련 작업은 모두 async로 한다. 유저가 누를때까지 기다려야하기 때문
final isLocationEnabled = await Geolocator
.isLocationServiceEnabled(); // 해당 기기가 위치 서비스를 사용중인지 확인 (앱이 아닌 디바이스 자체의 위치서비스)
if (!isLocationEnabled) {
return '위치 서비스를 활성화 해주세요.';
}
/*
* LocationPermission 타입
* denied : default >> Geolocator.requestPermission() 로 권한 요청 하면됨
* deniedForever : 권한을 허가하지 않은 것 >> 다시 앱이 권한을 요청할 수 없음 (유저가 직접 설정으로 가서 권한을 열도록 유도해야함)
* whileInUse : 앱 실행중에만 권한 허가
* always : 항상 허가
* */
LocationPermission checkedPermission =
await Geolocator.checkPermission(); // 앱의 위치서비스 권한 확인
if (checkedPermission == LocationPermission.denied) {
checkedPermission = await Geolocator.requestPermission();
if (checkedPermission == LocationPermission.denied) {
return '위치 권한을 허가해주세요.';
}
}
if (checkedPermission == LocationPermission.deniedForever) {
return '앱의 위치 권한을 세팅에서 허가해주세요.';
}
return '위치 권한이 허가되었습니다.';
}
AppBar renderAppBar() {
return AppBar(
title: Text(
'오늘도 출근',
style: TextStyle(
color: Colors.blue,
fontWeight: FontWeight.w700,
),
),
backgroundColor: Colors.white,
);
}
}
class _CustomGoogleMap extends StatelessWidget {
final CameraPosition initialPosition;
const _CustomGoogleMap({
required this.initialPosition,
super.key,
});
@override
Widget build(BuildContext context) {
return Expanded(
flex: 2,
child: GoogleMap(
mapType: MapType.normal,
// hybrid - 위성 지도 , normal - 일반 지도, terrain - 고저 지도 등
initialCameraPosition: initialPosition,
),
);
}
}
class _ChoolCheckButton extends StatelessWidget {
const _ChoolCheckButton({super.key});
@override
Widget build(BuildContext context) {
return Expanded(
child: Text(
'출근',
),
);
}
}
import 'package:flutter/material.dart'; import 'package:geolocator/geolocator.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @override State<HomeScreen> createState() => _HomeScreenState(); } class _HomeScreenState extends State<HomeScreen> { // latitude 위도 , longitude 경도 static final LatLng companyLatLng = LatLng(10.800715, 106.7120223); static final CameraPosition initialPosition = CameraPosition( target: companyLatLng, // 최초 포지션 zoom: 15, // zoom level ); @override Widget build(BuildContext context) { return Scaffold( appBar: renderAppBar(), body: FutureBuilder( // FutureBuilder를 사용하여 리턴되는 값과 Future를 가져오는 함수의 상태에 따라 다른 UI를 렌더링 할수 있다. future: checkPermission(), // Future 를 리턴해주는 아무 함수나 넣을 수 있다. builder: (BuildContext context, AsyncSnapshot snapshot) { // future 에 넣은 함수의 리턴값은 snapshot에 들어간다. print(snapshot.connectionState); // none, waiting, active, done (future 함수의 진행상태) print(snapshot.data); // '위치 권한이 허가되었습니다' if (snapshot.connectionState == ConnectionState.waiting) { // 아직 사용자의 응답을 기다리고 있을때는 로딩바 return Center( child: CircularProgressIndicator(), ); } if (snapshot.data == '위치 권한이 허가되었습니다.') { return Column( children: [ _CustomGoogleMap( initialPosition: initialPosition, ), _ChoolCheckButton(), ], ); } return Center( child: Text(snapshot.data), ); }, ), ); } Future<String> checkPermission() async { // 권한 관련 작업은 모두 async로 한다. 유저가 누를때까지 기다려야하기 때문 final isLocationEnabled = await Geolocator .isLocationServiceEnabled(); // 해당 기기가 위치 서비스를 사용중인지 확인 (앱이 아닌 디바이스 자체의 위치서비스) if (!isLocationEnabled) { return '위치 서비스를 활성화 해주세요.'; } /* * LocationPermission 타입 * denied : default >> Geolocator.requestPermission() 로 권한 요청 하면됨 * deniedForever : 권한을 허가하지 않은 것 >> 다시 앱이 권한을 요청할 수 없음 (유저가 직접 설정으로 가서 권한을 열도록 유도해야함) * whileInUse : 앱 실행중에만 권한 허가 * always : 항상 허가 * */ LocationPermission checkedPermission = await Geolocator.checkPermission(); // 앱의 위치서비스 권한 확인 if (checkedPermission == LocationPermission.denied) { checkedPermission = await Geolocator.requestPermission(); if (checkedPermission == LocationPermission.denied) { return '위치 권한을 허가해주세요.'; } } if (checkedPermission == LocationPermission.deniedForever) { return '앱의 위치 권한을 세팅에서 허가해주세요.'; } return '위치 권한이 허가되었습니다.'; } AppBar renderAppBar() { return AppBar( title: Text( '오늘도 출근', style: TextStyle( color: Colors.blue, fontWeight: FontWeight.w700, ), ), backgroundColor: Colors.white, ); } } class _CustomGoogleMap extends StatelessWidget { final CameraPosition initialPosition; const _CustomGoogleMap({ required this.initialPosition, super.key, }); @override Widget build(BuildContext context) { return Expanded( flex: 2, child: GoogleMap( mapType: MapType.normal, // hybrid - 위성 지도 , normal - 일반 지도, terrain - 고저 지도 등 initialCameraPosition: initialPosition, ), ); } } class _ChoolCheckButton extends StatelessWidget { const _ChoolCheckButton({super.key}); @override Widget build(BuildContext context) { return Expanded( child: Text( '출근', ), ); } }
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';

class HomeScreen extends StatefulWidget {
  const HomeScreen({super.key});

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  // latitude 위도 , longitude 경도
  static final LatLng companyLatLng = LatLng(10.800715, 106.7120223);

  static final CameraPosition initialPosition = CameraPosition(
    target: companyLatLng, // 최초 포지션
    zoom: 15, // zoom level
  );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: renderAppBar(),
      body: FutureBuilder( // FutureBuilder를 사용하여 리턴되는 값과 Future를 가져오는 함수의 상태에 따라 다른 UI를 렌더링 할수 있다.
        future: checkPermission(), // Future 를 리턴해주는 아무 함수나 넣을 수 있다.
        builder: (BuildContext context, AsyncSnapshot snapshot) {
          // future 에 넣은 함수의 리턴값은 snapshot에 들어간다.
          print(snapshot.connectionState); // none, waiting, active, done (future 함수의 진행상태)
          print(snapshot.data); // '위치 권한이 허가되었습니다'

          if (snapshot.connectionState == ConnectionState.waiting) {
            // 아직 사용자의 응답을 기다리고 있을때는 로딩바
            return Center(
              child: CircularProgressIndicator(),
            );
          }

          if (snapshot.data == '위치 권한이 허가되었습니다.') {
            return Column(
              children: [
                _CustomGoogleMap(
                  initialPosition: initialPosition,
                ),
                _ChoolCheckButton(),
              ],
            );
          }

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

  Future<String> checkPermission() async {
    // 권한 관련 작업은 모두 async로 한다. 유저가 누를때까지 기다려야하기 때문
    final isLocationEnabled = await Geolocator
        .isLocationServiceEnabled(); // 해당 기기가 위치 서비스를 사용중인지 확인 (앱이 아닌 디바이스 자체의 위치서비스)

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

    /*
    * LocationPermission 타입
    * denied : default >> Geolocator.requestPermission() 로 권한 요청 하면됨
    * deniedForever : 권한을 허가하지 않은 것 >> 다시 앱이 권한을 요청할 수 없음 (유저가 직접 설정으로 가서 권한을 열도록 유도해야함)
    * whileInUse : 앱 실행중에만 권한 허가
    * always : 항상 허가
    * */

    LocationPermission checkedPermission =
        await Geolocator.checkPermission(); // 앱의 위치서비스 권한 확인
    if (checkedPermission == LocationPermission.denied) {
      checkedPermission = await Geolocator.requestPermission();

      if (checkedPermission == LocationPermission.denied) {
        return '위치 권한을 허가해주세요.';
      }
    }

    if (checkedPermission == LocationPermission.deniedForever) {
      return '앱의 위치 권한을 세팅에서 허가해주세요.';
    }

    return '위치 권한이 허가되었습니다.';
  }

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

class _CustomGoogleMap extends StatelessWidget {
  final CameraPosition initialPosition;

  const _CustomGoogleMap({
    required this.initialPosition,
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    return Expanded(
      flex: 2,
      child: GoogleMap(
        mapType: MapType.normal,
        // hybrid - 위성 지도 , normal - 일반 지도, terrain - 고저 지도 등
        initialCameraPosition: initialPosition,
      ),
    );
  }
}

class _ChoolCheckButton extends StatelessWidget {
  const _ChoolCheckButton({super.key});

  @override
  Widget build(BuildContext context) {
    return Expanded(
      child: Text(
        '출근',
      ),
    );
  }
}

지도에 원 그리기

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
static final double distance = 100;
static final Circle circle = Circle(
// circle 생성
circleId: CircleId('circle'),
center: companyLatLng,
fillColor: Colors.blue.withOpacity(0.5), // 원 내부 색깔
radius: distance,
strokeColor: Colors.blue,
strokeWidth: 1,
);
....생략
class _CustomGoogleMap extends StatelessWidget {
final CameraPosition initialPosition;
final Circle circle;
const _CustomGoogleMap({
required this.initialPosition,
required this.circle,
super.key,
});
@override
Widget build(BuildContext context) {
return Expanded(
flex: 2,
child: GoogleMap(
mapType: MapType.normal,
// hybrid - 위성 지도 , normal - 일반 지도, terrain - 고저 지도 등
initialCameraPosition: initialPosition,
myLocationEnabled: true,
myLocationButtonEnabled: false,
circles: Set.from([circle]),
),
);
}
}
static final double distance = 100; static final Circle circle = Circle( // circle 생성 circleId: CircleId('circle'), center: companyLatLng, fillColor: Colors.blue.withOpacity(0.5), // 원 내부 색깔 radius: distance, strokeColor: Colors.blue, strokeWidth: 1, ); ....생략 class _CustomGoogleMap extends StatelessWidget { final CameraPosition initialPosition; final Circle circle; const _CustomGoogleMap({ required this.initialPosition, required this.circle, super.key, }); @override Widget build(BuildContext context) { return Expanded( flex: 2, child: GoogleMap( mapType: MapType.normal, // hybrid - 위성 지도 , normal - 일반 지도, terrain - 고저 지도 등 initialCameraPosition: initialPosition, myLocationEnabled: true, myLocationButtonEnabled: false, circles: Set.from([circle]), ), ); } }
  static final double distance = 100;
  static final Circle circle = Circle(
    // circle 생성
    circleId: CircleId('circle'),
    center: companyLatLng,
    fillColor: Colors.blue.withOpacity(0.5), // 원 내부 색깔
    radius: distance,
    strokeColor: Colors.blue,
    strokeWidth: 1,
  );

....생략



class _CustomGoogleMap extends StatelessWidget {
  final CameraPosition initialPosition;
  final Circle circle;

  const _CustomGoogleMap({
    required this.initialPosition,
    required this.circle,
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    return Expanded(
      flex: 2,
      child: GoogleMap(
        mapType: MapType.normal,
        // hybrid - 위성 지도 , normal - 일반 지도, terrain - 고저 지도 등
        initialCameraPosition: initialPosition,
        myLocationEnabled: true,
        myLocationButtonEnabled: false,
        circles: Set.from([circle]),
      ),
    );
  }
}

마커 심기

marker도 circle 과 완전 비슷함

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
static final Marker marker = Marker(
markerId: MarkerId('marker'),
position: companyLatLng,
);
...
class _CustomGoogleMap extends StatelessWidget {
final CameraPosition initialPosition;
final Circle circle;
final Marker marker;
const _CustomGoogleMap({
required this.initialPosition,
required this.circle,
required this.marker,
super.key,
});
@override
Widget build(BuildContext context) {
return Expanded(
flex: 2,
child: GoogleMap(
mapType: MapType.normal,
// hybrid - 위성 지도 , normal - 일반 지도, terrain - 고저 지도 등
initialCameraPosition: initialPosition,
myLocationEnabled: true,
myLocationButtonEnabled: false,
circles: Set.from([circle]),
markers: Set.from([marker]),
),
);
}
}
static final Marker marker = Marker( markerId: MarkerId('marker'), position: companyLatLng, ); ... class _CustomGoogleMap extends StatelessWidget { final CameraPosition initialPosition; final Circle circle; final Marker marker; const _CustomGoogleMap({ required this.initialPosition, required this.circle, required this.marker, super.key, }); @override Widget build(BuildContext context) { return Expanded( flex: 2, child: GoogleMap( mapType: MapType.normal, // hybrid - 위성 지도 , normal - 일반 지도, terrain - 고저 지도 등 initialCameraPosition: initialPosition, myLocationEnabled: true, myLocationButtonEnabled: false, circles: Set.from([circle]), markers: Set.from([marker]), ), ); } }
  static final Marker marker = Marker(
    markerId: MarkerId('marker'),
    position: companyLatLng,
  );

...

class _CustomGoogleMap extends StatelessWidget {
  final CameraPosition initialPosition;
  final Circle circle;
  final Marker marker;

  const _CustomGoogleMap({
    required this.initialPosition,
    required this.circle,
    required this.marker,
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    return Expanded(
      flex: 2,
      child: GoogleMap(
        mapType: MapType.normal,
        // hybrid - 위성 지도 , normal - 일반 지도, terrain - 고저 지도 등
        initialCameraPosition: initialPosition,
        myLocationEnabled: true,
        myLocationButtonEnabled: false,
        circles: Set.from([circle]),
        markers: Set.from([marker]),
      ),
    );
  }
}

현재 위치에 따라 원 색상 변화시키기

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
static final double okDistance = 100;
static final Circle withInDistanceCircle = Circle(
circleId: CircleId('withInDistanceCircle'),
center: companyLatLng,
fillColor: Colors.blue.withOpacity(0.5),
radius: okDistance,
strokeColor: Colors.blue,
strokeWidth: 1,
);
static final Circle notWithInDistanceCircle = Circle(
circleId: CircleId('notWithInDistanceCircle'),
center: companyLatLng,
fillColor: Colors.red.withOpacity(0.5),
radius: okDistance,
strokeColor: Colors.red,
strokeWidth: 1,
);
static final Circle checkDoneCircle = Circle(
circleId: CircleId('checkDoneCircle'),
center: companyLatLng,
fillColor: Colors.green.withOpacity(0.5),
radius: okDistance,
strokeColor: Colors.green,
strokeWidth: 1,
);
...
if (snapshot.data == '위치 권한이 허가되었습니다.') {
return StreamBuilder<Position>(
// FutureBuilder 와 같음
stream: Geolocator.getPositionStream(),
// position 이 변경될때마다 새로운 위치가 yield 되며, 제너릭 타입은 Position을 반환함 > build가 다시 실행됨
builder: (context, snapshot) {
bool isWithinRange = false;
if (snapshot.hasData) {
final start = snapshot.data!;
final end = companyLatLng;
final distance = Geolocator.distanceBetween(
start.latitude,
start.longitude,
end.latitude,
end.longitude,
); // 현재 거리와 회사 거리가 100m 이내인지 계산
if (distance < okDistance) {
isWithinRange = true;
}
}
return Column(
children: [
_CustomGoogleMap(
initialPosition: initialPosition,
circle: isWithinRange
? withInDistanceCircle
: notWithInDistanceCircle,
marker: marker,
),
_ChoolCheckButton(),
],
);
});
}
static final double okDistance = 100; static final Circle withInDistanceCircle = Circle( circleId: CircleId('withInDistanceCircle'), center: companyLatLng, fillColor: Colors.blue.withOpacity(0.5), radius: okDistance, strokeColor: Colors.blue, strokeWidth: 1, ); static final Circle notWithInDistanceCircle = Circle( circleId: CircleId('notWithInDistanceCircle'), center: companyLatLng, fillColor: Colors.red.withOpacity(0.5), radius: okDistance, strokeColor: Colors.red, strokeWidth: 1, ); static final Circle checkDoneCircle = Circle( circleId: CircleId('checkDoneCircle'), center: companyLatLng, fillColor: Colors.green.withOpacity(0.5), radius: okDistance, strokeColor: Colors.green, strokeWidth: 1, ); ... if (snapshot.data == '위치 권한이 허가되었습니다.') { return StreamBuilder<Position>( // FutureBuilder 와 같음 stream: Geolocator.getPositionStream(), // position 이 변경될때마다 새로운 위치가 yield 되며, 제너릭 타입은 Position을 반환함 > build가 다시 실행됨 builder: (context, snapshot) { bool isWithinRange = false; if (snapshot.hasData) { final start = snapshot.data!; final end = companyLatLng; final distance = Geolocator.distanceBetween( start.latitude, start.longitude, end.latitude, end.longitude, ); // 현재 거리와 회사 거리가 100m 이내인지 계산 if (distance < okDistance) { isWithinRange = true; } } return Column( children: [ _CustomGoogleMap( initialPosition: initialPosition, circle: isWithinRange ? withInDistanceCircle : notWithInDistanceCircle, marker: marker, ), _ChoolCheckButton(), ], ); }); }
static final double okDistance = 100;
  static final Circle withInDistanceCircle = Circle(
    circleId: CircleId('withInDistanceCircle'),
    center: companyLatLng,
    fillColor: Colors.blue.withOpacity(0.5),
    radius: okDistance,
    strokeColor: Colors.blue,
    strokeWidth: 1,
  );
  static final Circle notWithInDistanceCircle = Circle(
    circleId: CircleId('notWithInDistanceCircle'),
    center: companyLatLng,
    fillColor: Colors.red.withOpacity(0.5),
    radius: okDistance,
    strokeColor: Colors.red,
    strokeWidth: 1,
  );
  static final Circle checkDoneCircle = Circle(
    circleId: CircleId('checkDoneCircle'),
    center: companyLatLng,
    fillColor: Colors.green.withOpacity(0.5),
    radius: okDistance,
    strokeColor: Colors.green,
    strokeWidth: 1,
  );

...

if (snapshot.data == '위치 권한이 허가되었습니다.') {
            return StreamBuilder<Position>(
                // FutureBuilder 와 같음
                stream: Geolocator.getPositionStream(),
                // position 이 변경될때마다 새로운 위치가 yield 되며, 제너릭 타입은 Position을 반환함 > build가 다시 실행됨
                builder: (context, snapshot) {
                  bool isWithinRange = false;

                  if (snapshot.hasData) {
                    final start = snapshot.data!;
                    final end = companyLatLng;
                    final distance = Geolocator.distanceBetween(
                      start.latitude,
                      start.longitude,
                      end.latitude,
                      end.longitude,
                    ); // 현재 거리와 회사 거리가 100m 이내인지 계산

                    if (distance < okDistance) {
                      isWithinRange = true;
                    }
                  }
                  return Column(
                    children: [
                      _CustomGoogleMap(
                        initialPosition: initialPosition,
                        circle: isWithinRange
                            ? withInDistanceCircle
                            : notWithInDistanceCircle,
                        marker: marker,
                      ),
                      _ChoolCheckButton(),
                    ],
                  );
                });
          }

Dialog 로 출근 여부 받기 / 상태에 따라 원 변화시키기

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
bool choolCheckDone = false;
// latitude 위도 , longitude 경도
static final LatLng companyLatLng = LatLng(
10.8007,
106.7146,
);
static final CameraPosition initialPosition = CameraPosition(
target: companyLatLng, // 최초 포지션
zoom: 15, // zoom level
);
static final double okDistance = 100;
static final Circle withInDistanceCircle = Circle(
circleId: CircleId('withInDistanceCircle'),
center: companyLatLng,
fillColor: Colors.blue.withOpacity(0.5),
radius: okDistance,
strokeColor: Colors.blue,
strokeWidth: 1,
);
static final Circle notWithInDistanceCircle = Circle(
circleId: CircleId('notWithInDistanceCircle'),
center: companyLatLng,
fillColor: Colors.red.withOpacity(0.5),
radius: okDistance,
strokeColor: Colors.red,
strokeWidth: 1,
);
static final Circle checkDoneCircle = Circle(
circleId: CircleId('checkDoneCircle'),
center: companyLatLng,
fillColor: Colors.green.withOpacity(0.5),
radius: okDistance,
strokeColor: Colors.green,
strokeWidth: 1,
);
static final Marker marker = Marker(
markerId: MarkerId('marker'),
position: companyLatLng,
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: renderAppBar(),
body: FutureBuilder<String>(
// FutureBuilder를 사용하여 리턴되는 값과 Future를 가져오는 함수의 상태에 따라 다른 UI를 렌더링 할수 있다.
future: checkPermission(), // Future 를 리턴해주는 아무 함수나 넣을 수 있다.
builder: (BuildContext context, AsyncSnapshot snapshot) {
// future 에 넣은 함수의 리턴값은 snapshot에 들어간다.
print(snapshot
.connectionState); // none, waiting, active, done (future 함수의 진행상태)
print(snapshot.data); // '위치 권한이 허가되었습니다'
if (snapshot.connectionState == ConnectionState.waiting) {
// 아직 사용자의 응답을 기다리고 있을때는 로딩바
return Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.data == '위치 권한이 허가되었습니다.') {
return StreamBuilder<Position>(
// FutureBuilder 와 같음
stream: Geolocator.getPositionStream(),
// position 이 변경될때마다 새로운 위치가 yield 되며, 제너릭 타입은 Position을 반환함 > build가 다시 실행됨
builder: (context, snapshot) {
bool isWithinRange = false;
if (snapshot.hasData) {
final start = snapshot.data!;
final end = companyLatLng;
final distance = Geolocator.distanceBetween(
start.latitude,
start.longitude,
end.latitude,
end.longitude,
); // 현재 거리와 회사 거리가 100m 이내인지 계산
if (distance < okDistance) {
isWithinRange = true;
}
}
return Column(
children: [
_CustomGoogleMap(
initialPosition: initialPosition,
circle: choolCheckDone
? checkDoneCircle
: isWithinRange
? withInDistanceCircle
: notWithInDistanceCircle,
marker: marker,
),
_ChoolCheckButton(
isWithinRange: isWithinRange,
choolCheckDone: choolCheckDone,
onPressed: onChoolCheckPressed,
),
],
);
});
}
return Center(
child: Text(snapshot.data),
);
},
),
);
}
onChoolCheckPressed() async {
final result = await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
// alert dialog 를 쉽게 제작할 수 있음
title: Text('출근하기'),
content: Text('출근을 하시겠습니까?'),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop(
false); // dialog 를 하나의 스크린처럼 생각하면됨. 스크린 전환에서 뒤돌아가기 처럼 pop()
},
child: Text('취소'),
),
TextButton(
onPressed: () {
Navigator.of(context).pop(true);
},
child: Text('출근하기'),
)
],
);
},
);
if (result) {
setState(() {
choolCheckDone = true;
});
}
}
Future<String> checkPermission() async {
// 권한 관련 작업은 모두 async로 한다. 유저가 누를때까지 기다려야하기 때문
final isLocationEnabled = await Geolocator
.isLocationServiceEnabled(); // 해당 기기가 위치 서비스를 사용중인지 확인 (앱이 아닌 디바이스 자체의 위치서비스)
if (!isLocationEnabled) {
return '위치 서비스를 활성화 해주세요.';
}
/*
* LocationPermission 타입
* denied : default >> Geolocator.requestPermission() 로 권한 요청 하면됨
* deniedForever : 권한을 허가하지 않은 것 >> 다시 앱이 권한을 요청할 수 없음 (유저가 직접 설정으로 가서 권한을 열도록 유도해야함)
* whileInUse : 앱 실행중에만 권한 허가
* always : 항상 허가
* */
LocationPermission checkedPermission =
await Geolocator.checkPermission(); // 앱의 위치서비스 권한 확인
if (checkedPermission == LocationPermission.denied) {
checkedPermission = await Geolocator.requestPermission();
if (checkedPermission == LocationPermission.denied) {
return '위치 권한을 허가해주세요.';
}
}
if (checkedPermission == LocationPermission.deniedForever) {
return '앱의 위치 권한을 세팅에서 허가해주세요.';
}
return '위치 권한이 허가되었습니다.';
}
AppBar renderAppBar() {
return AppBar(
title: Center(
child: Text(
'오늘도 출근',
style: TextStyle(
color: Colors.blue,
fontWeight: FontWeight.w700,
),
),
),
backgroundColor: Colors.white,
);
}
}
class _CustomGoogleMap extends StatelessWidget {
final CameraPosition initialPosition;
final Circle circle;
final Marker marker;
const _CustomGoogleMap({
required this.initialPosition,
required this.circle,
required this.marker,
super.key,
});
@override
Widget build(BuildContext context) {
return Expanded(
flex: 2,
child: GoogleMap(
mapType: MapType.normal,
initialCameraPosition: initialPosition,
myLocationEnabled: true,
myLocationButtonEnabled: false,
circles: Set.from([circle]),
markers: Set.from([marker]),
),
);
}
}
class _ChoolCheckButton extends StatelessWidget {
final bool isWithinRange;
final VoidCallback onPressed;
final bool choolCheckDone;
const _ChoolCheckButton({
required this.isWithinRange,
required this.onPressed,
required this.choolCheckDone,
super.key,
});
@override
Widget build(BuildContext context) {
return Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.timelapse_outlined,
size: 50.0,
color: choolCheckDone
? Colors.green
: isWithinRange
? Colors.blue
: Colors.red,
),
const SizedBox(height: 20.0),
if (!choolCheckDone && isWithinRange)
TextButton(
onPressed: onPressed,
child: Text('출근하기'),
),
],
),
);
}
}
import 'package:flutter/material.dart'; import 'package:geolocator/geolocator.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @override State<HomeScreen> createState() => _HomeScreenState(); } class _HomeScreenState extends State<HomeScreen> { bool choolCheckDone = false; // latitude 위도 , longitude 경도 static final LatLng companyLatLng = LatLng( 10.8007, 106.7146, ); static final CameraPosition initialPosition = CameraPosition( target: companyLatLng, // 최초 포지션 zoom: 15, // zoom level ); static final double okDistance = 100; static final Circle withInDistanceCircle = Circle( circleId: CircleId('withInDistanceCircle'), center: companyLatLng, fillColor: Colors.blue.withOpacity(0.5), radius: okDistance, strokeColor: Colors.blue, strokeWidth: 1, ); static final Circle notWithInDistanceCircle = Circle( circleId: CircleId('notWithInDistanceCircle'), center: companyLatLng, fillColor: Colors.red.withOpacity(0.5), radius: okDistance, strokeColor: Colors.red, strokeWidth: 1, ); static final Circle checkDoneCircle = Circle( circleId: CircleId('checkDoneCircle'), center: companyLatLng, fillColor: Colors.green.withOpacity(0.5), radius: okDistance, strokeColor: Colors.green, strokeWidth: 1, ); static final Marker marker = Marker( markerId: MarkerId('marker'), position: companyLatLng, ); @override Widget build(BuildContext context) { return Scaffold( appBar: renderAppBar(), body: FutureBuilder<String>( // FutureBuilder를 사용하여 리턴되는 값과 Future를 가져오는 함수의 상태에 따라 다른 UI를 렌더링 할수 있다. future: checkPermission(), // Future 를 리턴해주는 아무 함수나 넣을 수 있다. builder: (BuildContext context, AsyncSnapshot snapshot) { // future 에 넣은 함수의 리턴값은 snapshot에 들어간다. print(snapshot .connectionState); // none, waiting, active, done (future 함수의 진행상태) print(snapshot.data); // '위치 권한이 허가되었습니다' if (snapshot.connectionState == ConnectionState.waiting) { // 아직 사용자의 응답을 기다리고 있을때는 로딩바 return Center( child: CircularProgressIndicator(), ); } if (snapshot.data == '위치 권한이 허가되었습니다.') { return StreamBuilder<Position>( // FutureBuilder 와 같음 stream: Geolocator.getPositionStream(), // position 이 변경될때마다 새로운 위치가 yield 되며, 제너릭 타입은 Position을 반환함 > build가 다시 실행됨 builder: (context, snapshot) { bool isWithinRange = false; if (snapshot.hasData) { final start = snapshot.data!; final end = companyLatLng; final distance = Geolocator.distanceBetween( start.latitude, start.longitude, end.latitude, end.longitude, ); // 현재 거리와 회사 거리가 100m 이내인지 계산 if (distance < okDistance) { isWithinRange = true; } } return Column( children: [ _CustomGoogleMap( initialPosition: initialPosition, circle: choolCheckDone ? checkDoneCircle : isWithinRange ? withInDistanceCircle : notWithInDistanceCircle, marker: marker, ), _ChoolCheckButton( isWithinRange: isWithinRange, choolCheckDone: choolCheckDone, onPressed: onChoolCheckPressed, ), ], ); }); } return Center( child: Text(snapshot.data), ); }, ), ); } onChoolCheckPressed() async { final result = await showDialog( context: context, builder: (BuildContext context) { return AlertDialog( // alert dialog 를 쉽게 제작할 수 있음 title: Text('출근하기'), content: Text('출근을 하시겠습니까?'), actions: [ TextButton( onPressed: () { Navigator.of(context).pop( false); // dialog 를 하나의 스크린처럼 생각하면됨. 스크린 전환에서 뒤돌아가기 처럼 pop() }, child: Text('취소'), ), TextButton( onPressed: () { Navigator.of(context).pop(true); }, child: Text('출근하기'), ) ], ); }, ); if (result) { setState(() { choolCheckDone = true; }); } } Future<String> checkPermission() async { // 권한 관련 작업은 모두 async로 한다. 유저가 누를때까지 기다려야하기 때문 final isLocationEnabled = await Geolocator .isLocationServiceEnabled(); // 해당 기기가 위치 서비스를 사용중인지 확인 (앱이 아닌 디바이스 자체의 위치서비스) if (!isLocationEnabled) { return '위치 서비스를 활성화 해주세요.'; } /* * LocationPermission 타입 * denied : default >> Geolocator.requestPermission() 로 권한 요청 하면됨 * deniedForever : 권한을 허가하지 않은 것 >> 다시 앱이 권한을 요청할 수 없음 (유저가 직접 설정으로 가서 권한을 열도록 유도해야함) * whileInUse : 앱 실행중에만 권한 허가 * always : 항상 허가 * */ LocationPermission checkedPermission = await Geolocator.checkPermission(); // 앱의 위치서비스 권한 확인 if (checkedPermission == LocationPermission.denied) { checkedPermission = await Geolocator.requestPermission(); if (checkedPermission == LocationPermission.denied) { return '위치 권한을 허가해주세요.'; } } if (checkedPermission == LocationPermission.deniedForever) { return '앱의 위치 권한을 세팅에서 허가해주세요.'; } return '위치 권한이 허가되었습니다.'; } AppBar renderAppBar() { return AppBar( title: Center( child: Text( '오늘도 출근', style: TextStyle( color: Colors.blue, fontWeight: FontWeight.w700, ), ), ), backgroundColor: Colors.white, ); } } class _CustomGoogleMap extends StatelessWidget { final CameraPosition initialPosition; final Circle circle; final Marker marker; const _CustomGoogleMap({ required this.initialPosition, required this.circle, required this.marker, super.key, }); @override Widget build(BuildContext context) { return Expanded( flex: 2, child: GoogleMap( mapType: MapType.normal, initialCameraPosition: initialPosition, myLocationEnabled: true, myLocationButtonEnabled: false, circles: Set.from([circle]), markers: Set.from([marker]), ), ); } } class _ChoolCheckButton extends StatelessWidget { final bool isWithinRange; final VoidCallback onPressed; final bool choolCheckDone; const _ChoolCheckButton({ required this.isWithinRange, required this.onPressed, required this.choolCheckDone, super.key, }); @override Widget build(BuildContext context) { return Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.timelapse_outlined, size: 50.0, color: choolCheckDone ? Colors.green : isWithinRange ? Colors.blue : Colors.red, ), const SizedBox(height: 20.0), if (!choolCheckDone && isWithinRange) TextButton( onPressed: onPressed, child: Text('출근하기'), ), ], ), ); } }
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';

class HomeScreen extends StatefulWidget {
  const HomeScreen({super.key});

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  bool choolCheckDone = false;

  // latitude 위도 , longitude 경도
  static final LatLng companyLatLng = LatLng(
    10.8007,
    106.7146,
  );

  static final CameraPosition initialPosition = CameraPosition(
    target: companyLatLng, // 최초 포지션
    zoom: 15, // zoom level
  );

  static final double okDistance = 100;
  static final Circle withInDistanceCircle = Circle(
    circleId: CircleId('withInDistanceCircle'),
    center: companyLatLng,
    fillColor: Colors.blue.withOpacity(0.5),
    radius: okDistance,
    strokeColor: Colors.blue,
    strokeWidth: 1,
  );
  static final Circle notWithInDistanceCircle = Circle(
    circleId: CircleId('notWithInDistanceCircle'),
    center: companyLatLng,
    fillColor: Colors.red.withOpacity(0.5),
    radius: okDistance,
    strokeColor: Colors.red,
    strokeWidth: 1,
  );
  static final Circle checkDoneCircle = Circle(
    circleId: CircleId('checkDoneCircle'),
    center: companyLatLng,
    fillColor: Colors.green.withOpacity(0.5),
    radius: okDistance,
    strokeColor: Colors.green,
    strokeWidth: 1,
  );

  static final Marker marker = Marker(
    markerId: MarkerId('marker'),
    position: companyLatLng,
  );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: renderAppBar(),
      body: FutureBuilder<String>(
        // FutureBuilder를 사용하여 리턴되는 값과 Future를 가져오는 함수의 상태에 따라 다른 UI를 렌더링 할수 있다.
        future: checkPermission(), // Future 를 리턴해주는 아무 함수나 넣을 수 있다.
        builder: (BuildContext context, AsyncSnapshot snapshot) {
          // future 에 넣은 함수의 리턴값은 snapshot에 들어간다.
          print(snapshot
              .connectionState); // none, waiting, active, done (future 함수의 진행상태)
          print(snapshot.data); // '위치 권한이 허가되었습니다'

          if (snapshot.connectionState == ConnectionState.waiting) {
            // 아직 사용자의 응답을 기다리고 있을때는 로딩바
            return Center(
              child: CircularProgressIndicator(),
            );
          }

          if (snapshot.data == '위치 권한이 허가되었습니다.') {
            return StreamBuilder<Position>(
                // FutureBuilder 와 같음
                stream: Geolocator.getPositionStream(),
                // position 이 변경될때마다 새로운 위치가 yield 되며, 제너릭 타입은 Position을 반환함 > build가 다시 실행됨
                builder: (context, snapshot) {
                  bool isWithinRange = false;

                  if (snapshot.hasData) {
                    final start = snapshot.data!;
                    final end = companyLatLng;
                    final distance = Geolocator.distanceBetween(
                      start.latitude,
                      start.longitude,
                      end.latitude,
                      end.longitude,
                    ); // 현재 거리와 회사 거리가 100m 이내인지 계산

                    if (distance < okDistance) {
                      isWithinRange = true;
                    }
                  }
                  return Column(
                    children: [
                      _CustomGoogleMap(
                        initialPosition: initialPosition,
                        circle: choolCheckDone
                            ? checkDoneCircle
                            : isWithinRange
                                ? withInDistanceCircle
                                : notWithInDistanceCircle,
                        marker: marker,
                      ),
                      _ChoolCheckButton(
                        isWithinRange: isWithinRange,
                        choolCheckDone: choolCheckDone,
                        onPressed: onChoolCheckPressed,
                      ),
                    ],
                  );
                });
          }

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

  onChoolCheckPressed() async {
    final result = await showDialog(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          // alert dialog 를 쉽게 제작할 수 있음
          title: Text('출근하기'),
          content: Text('출근을 하시겠습니까?'),
          actions: [
            TextButton(
              onPressed: () {
                Navigator.of(context).pop(
                    false); // dialog 를 하나의 스크린처럼 생각하면됨. 스크린 전환에서 뒤돌아가기 처럼 pop()
              },
              child: Text('취소'),
            ),
            TextButton(
              onPressed: () {
                Navigator.of(context).pop(true);
              },
              child: Text('출근하기'),
            )
          ],
        );
      },
    );

    if (result) {
      setState(() {
        choolCheckDone = true;
      });
    }
  }

  Future<String> checkPermission() async {
    // 권한 관련 작업은 모두 async로 한다. 유저가 누를때까지 기다려야하기 때문
    final isLocationEnabled = await Geolocator
        .isLocationServiceEnabled(); // 해당 기기가 위치 서비스를 사용중인지 확인 (앱이 아닌 디바이스 자체의 위치서비스)

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

    /*
    * LocationPermission 타입
    * denied : default >> Geolocator.requestPermission() 로 권한 요청 하면됨
    * deniedForever : 권한을 허가하지 않은 것 >> 다시 앱이 권한을 요청할 수 없음 (유저가 직접 설정으로 가서 권한을 열도록 유도해야함)
    * whileInUse : 앱 실행중에만 권한 허가
    * always : 항상 허가
    * */

    LocationPermission checkedPermission =
        await Geolocator.checkPermission(); // 앱의 위치서비스 권한 확인
    if (checkedPermission == LocationPermission.denied) {
      checkedPermission = await Geolocator.requestPermission();

      if (checkedPermission == LocationPermission.denied) {
        return '위치 권한을 허가해주세요.';
      }
    }

    if (checkedPermission == LocationPermission.deniedForever) {
      return '앱의 위치 권한을 세팅에서 허가해주세요.';
    }

    return '위치 권한이 허가되었습니다.';
  }

  AppBar renderAppBar() {
    return AppBar(
      title: Center(
        child: Text(
          '오늘도 출근',
          style: TextStyle(
            color: Colors.blue,
            fontWeight: FontWeight.w700,
          ),
        ),
      ),
      backgroundColor: Colors.white,
    );
  }
}

class _CustomGoogleMap extends StatelessWidget {
  final CameraPosition initialPosition;
  final Circle circle;
  final Marker marker;

  const _CustomGoogleMap({
    required this.initialPosition,
    required this.circle,
    required this.marker,
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    return Expanded(
      flex: 2,
      child: GoogleMap(
        mapType: MapType.normal,
        initialCameraPosition: initialPosition,
        myLocationEnabled: true,
        myLocationButtonEnabled: false,
        circles: Set.from([circle]),
        markers: Set.from([marker]),
      ),
    );
  }
}

class _ChoolCheckButton extends StatelessWidget {
  final bool isWithinRange;
  final VoidCallback onPressed;
  final bool choolCheckDone;

  const _ChoolCheckButton({
    required this.isWithinRange,
    required this.onPressed,
    required this.choolCheckDone,
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    return Expanded(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(
            Icons.timelapse_outlined,
            size: 50.0,
            color: choolCheckDone
                ? Colors.green
                : isWithinRange
                    ? Colors.blue
                    : Colors.red,
          ),
          const SizedBox(height: 20.0),
          if (!choolCheckDone && isWithinRange)
            TextButton(
              onPressed: onPressed,
              child: Text('출근하기'),
            ),
        ],
      ),
    );
  }
}

현재 위치로 이동 기능 추가 + 마지막 정리

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
bool choolCheckDone = false;
GoogleMapController? mapController;
// latitude 위도 , longitude 경도
static final LatLng companyLatLng = LatLng(
10.8007,
106.7146,
);
static final CameraPosition initialPosition = CameraPosition(
target: companyLatLng, // 최초 포지션
zoom: 15, // zoom level
);
static final double okDistance = 100;
static final Circle withInDistanceCircle = Circle(
circleId: CircleId('withInDistanceCircle'),
center: companyLatLng,
fillColor: Colors.blue.withOpacity(0.5),
radius: okDistance,
strokeColor: Colors.blue,
strokeWidth: 1,
);
static final Circle notWithInDistanceCircle = Circle(
circleId: CircleId('notWithInDistanceCircle'),
center: companyLatLng,
fillColor: Colors.red.withOpacity(0.5),
radius: okDistance,
strokeColor: Colors.red,
strokeWidth: 1,
);
static final Circle checkDoneCircle = Circle(
circleId: CircleId('checkDoneCircle'),
center: companyLatLng,
fillColor: Colors.green.withOpacity(0.5),
radius: okDistance,
strokeColor: Colors.green,
strokeWidth: 1,
);
static final Marker marker = Marker(
markerId: MarkerId('marker'),
position: companyLatLng,
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: renderAppBar(),
body: FutureBuilder<String>(
// FutureBuilder를 사용하여 리턴되는 값과 Future를 가져오는 함수의 상태에 따라 다른 UI를 렌더링 할수 있다.
future: checkPermission(), // Future 를 리턴해주는 아무 함수나 넣을 수 있다.
builder: (BuildContext context, AsyncSnapshot snapshot) {
// future 에 넣은 함수의 리턴값은 snapshot에 들어간다.
print(snapshot
.connectionState); // none, waiting, active, done (future 함수의 진행상태)
print(snapshot.data); // '위치 권한이 허가되었습니다'
if (snapshot.connectionState == ConnectionState.waiting) {
// 아직 사용자의 응답을 기다리고 있을때는 로딩바
return Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.data == '위치 권한이 허가되었습니다.') {
return StreamBuilder<Position>(
// FutureBuilder 와 같음
stream: Geolocator.getPositionStream(),
// position 이 변경될때마다 새로운 위치가 yield 되며, 제너릭 타입은 Position을 반환함 > build가 다시 실행됨
builder: (context, snapshot) {
bool isWithinRange = false;
if (snapshot.hasData) {
final start = snapshot.data!;
final end = companyLatLng;
final distance = Geolocator.distanceBetween(
start.latitude,
start.longitude,
end.latitude,
end.longitude,
); // 현재 거리와 회사 거리가 100m 이내인지 계산
if (distance < okDistance) {
isWithinRange = true;
}
}
return Column(
children: [
_CustomGoogleMap(
initialPosition: initialPosition,
circle: choolCheckDone
? checkDoneCircle
: isWithinRange
? withInDistanceCircle
: notWithInDistanceCircle,
marker: marker,
onMapCreated: onMapCreated,
),
_ChoolCheckButton(
isWithinRange: isWithinRange,
choolCheckDone: choolCheckDone,
onPressed: onChoolCheckPressed,
),
],
);
});
}
return Center(
child: Text(snapshot.data),
);
},
),
);
}
onMapCreated(GoogleMapController controller) {
mapController = controller;
}
onChoolCheckPressed() async {
final result = await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
// alert dialog 를 쉽게 제작할 수 있음
title: Text('출근하기'),
content: Text('출근을 하시겠습니까?'),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop(
false); // dialog 를 하나의 스크린처럼 생각하면됨. 스크린 전환에서 뒤돌아가기 처럼 pop()
},
child: Text('취소'),
),
TextButton(
onPressed: () {
Navigator.of(context).pop(true);
},
child: Text('출근하기'),
)
],
);
},
);
if (result) {
setState(() {
choolCheckDone = true;
});
}
}
Future<String> checkPermission() async {
// 권한 관련 작업은 모두 async로 한다. 유저가 누를때까지 기다려야하기 때문
final isLocationEnabled = await Geolocator
.isLocationServiceEnabled(); // 해당 기기가 위치 서비스를 사용중인지 확인 (앱이 아닌 디바이스 자체의 위치서비스)
if (!isLocationEnabled) {
return '위치 서비스를 활성화 해주세요.';
}
/*
* LocationPermission 타입
* denied : default >> Geolocator.requestPermission() 로 권한 요청 하면됨
* deniedForever : 권한을 허가하지 않은 것 >> 다시 앱이 권한을 요청할 수 없음 (유저가 직접 설정으로 가서 권한을 열도록 유도해야함)
* whileInUse : 앱 실행중에만 권한 허가
* always : 항상 허가
* */
LocationPermission checkedPermission =
await Geolocator.checkPermission(); // 앱의 위치서비스 권한 확인
if (checkedPermission == LocationPermission.denied) {
checkedPermission = await Geolocator.requestPermission();
if (checkedPermission == LocationPermission.denied) {
return '위치 권한을 허가해주세요.';
}
}
if (checkedPermission == LocationPermission.deniedForever) {
return '앱의 위치 권한을 세팅에서 허가해주세요.';
}
return '위치 권한이 허가되었습니다.';
}
AppBar renderAppBar() {
return AppBar(
title: Text(
'오늘도 출근',
style: TextStyle(
color: Colors.blue,
fontWeight: FontWeight.w700,
),
),
backgroundColor: Colors.white,
actions: [
IconButton(
onPressed: () async {
if (mapController == null) {
return;
}
final location = await Geolocator.getCurrentPosition(); // 현재 위치 구하기
mapController!.animateCamera(CameraUpdate.newLatLng(LatLng( // 현재 위치로 카메라 이동
location.latitude,
location.longitude,
)));
},
color: Colors.blue,
icon: Icon(Icons.my_location),
)
],
);
}
}
class _CustomGoogleMap extends StatelessWidget {
final CameraPosition initialPosition;
final Circle circle;
final Marker marker;
final MapCreatedCallback onMapCreated;
const _CustomGoogleMap({
required this.initialPosition,
required this.circle,
required this.marker,
required this.onMapCreated,
super.key,
});
@override
Widget build(BuildContext context) {
return Expanded(
flex: 2,
child: GoogleMap(
mapType: MapType.normal,
// hybrid - 위성 지도 , normal - 일반 지도, terrain - 고저 지도 등
initialCameraPosition: initialPosition,
myLocationEnabled: true,
myLocationButtonEnabled: false,
circles: Set.from([circle]),
markers: Set.from([marker]),
onMapCreated: onMapCreated,
),
);
}
}
class _ChoolCheckButton extends StatelessWidget {
final bool isWithinRange;
final VoidCallback onPressed;
final bool choolCheckDone;
const _ChoolCheckButton({
required this.isWithinRange,
required this.onPressed,
required this.choolCheckDone,
super.key,
});
@override
Widget build(BuildContext context) {
return Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.timelapse_outlined,
size: 50.0,
color: choolCheckDone
? Colors.green
: isWithinRange
? Colors.blue
: Colors.red,
),
const SizedBox(height: 20.0),
if (!choolCheckDone && isWithinRange)
TextButton(
onPressed: onPressed,
child: Text('출근하기'),
),
],
),
);
}
}
import 'package:flutter/material.dart'; import 'package:geolocator/geolocator.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @override State<HomeScreen> createState() => _HomeScreenState(); } class _HomeScreenState extends State<HomeScreen> { bool choolCheckDone = false; GoogleMapController? mapController; // latitude 위도 , longitude 경도 static final LatLng companyLatLng = LatLng( 10.8007, 106.7146, ); static final CameraPosition initialPosition = CameraPosition( target: companyLatLng, // 최초 포지션 zoom: 15, // zoom level ); static final double okDistance = 100; static final Circle withInDistanceCircle = Circle( circleId: CircleId('withInDistanceCircle'), center: companyLatLng, fillColor: Colors.blue.withOpacity(0.5), radius: okDistance, strokeColor: Colors.blue, strokeWidth: 1, ); static final Circle notWithInDistanceCircle = Circle( circleId: CircleId('notWithInDistanceCircle'), center: companyLatLng, fillColor: Colors.red.withOpacity(0.5), radius: okDistance, strokeColor: Colors.red, strokeWidth: 1, ); static final Circle checkDoneCircle = Circle( circleId: CircleId('checkDoneCircle'), center: companyLatLng, fillColor: Colors.green.withOpacity(0.5), radius: okDistance, strokeColor: Colors.green, strokeWidth: 1, ); static final Marker marker = Marker( markerId: MarkerId('marker'), position: companyLatLng, ); @override Widget build(BuildContext context) { return Scaffold( appBar: renderAppBar(), body: FutureBuilder<String>( // FutureBuilder를 사용하여 리턴되는 값과 Future를 가져오는 함수의 상태에 따라 다른 UI를 렌더링 할수 있다. future: checkPermission(), // Future 를 리턴해주는 아무 함수나 넣을 수 있다. builder: (BuildContext context, AsyncSnapshot snapshot) { // future 에 넣은 함수의 리턴값은 snapshot에 들어간다. print(snapshot .connectionState); // none, waiting, active, done (future 함수의 진행상태) print(snapshot.data); // '위치 권한이 허가되었습니다' if (snapshot.connectionState == ConnectionState.waiting) { // 아직 사용자의 응답을 기다리고 있을때는 로딩바 return Center( child: CircularProgressIndicator(), ); } if (snapshot.data == '위치 권한이 허가되었습니다.') { return StreamBuilder<Position>( // FutureBuilder 와 같음 stream: Geolocator.getPositionStream(), // position 이 변경될때마다 새로운 위치가 yield 되며, 제너릭 타입은 Position을 반환함 > build가 다시 실행됨 builder: (context, snapshot) { bool isWithinRange = false; if (snapshot.hasData) { final start = snapshot.data!; final end = companyLatLng; final distance = Geolocator.distanceBetween( start.latitude, start.longitude, end.latitude, end.longitude, ); // 현재 거리와 회사 거리가 100m 이내인지 계산 if (distance < okDistance) { isWithinRange = true; } } return Column( children: [ _CustomGoogleMap( initialPosition: initialPosition, circle: choolCheckDone ? checkDoneCircle : isWithinRange ? withInDistanceCircle : notWithInDistanceCircle, marker: marker, onMapCreated: onMapCreated, ), _ChoolCheckButton( isWithinRange: isWithinRange, choolCheckDone: choolCheckDone, onPressed: onChoolCheckPressed, ), ], ); }); } return Center( child: Text(snapshot.data), ); }, ), ); } onMapCreated(GoogleMapController controller) { mapController = controller; } onChoolCheckPressed() async { final result = await showDialog( context: context, builder: (BuildContext context) { return AlertDialog( // alert dialog 를 쉽게 제작할 수 있음 title: Text('출근하기'), content: Text('출근을 하시겠습니까?'), actions: [ TextButton( onPressed: () { Navigator.of(context).pop( false); // dialog 를 하나의 스크린처럼 생각하면됨. 스크린 전환에서 뒤돌아가기 처럼 pop() }, child: Text('취소'), ), TextButton( onPressed: () { Navigator.of(context).pop(true); }, child: Text('출근하기'), ) ], ); }, ); if (result) { setState(() { choolCheckDone = true; }); } } Future<String> checkPermission() async { // 권한 관련 작업은 모두 async로 한다. 유저가 누를때까지 기다려야하기 때문 final isLocationEnabled = await Geolocator .isLocationServiceEnabled(); // 해당 기기가 위치 서비스를 사용중인지 확인 (앱이 아닌 디바이스 자체의 위치서비스) if (!isLocationEnabled) { return '위치 서비스를 활성화 해주세요.'; } /* * LocationPermission 타입 * denied : default >> Geolocator.requestPermission() 로 권한 요청 하면됨 * deniedForever : 권한을 허가하지 않은 것 >> 다시 앱이 권한을 요청할 수 없음 (유저가 직접 설정으로 가서 권한을 열도록 유도해야함) * whileInUse : 앱 실행중에만 권한 허가 * always : 항상 허가 * */ LocationPermission checkedPermission = await Geolocator.checkPermission(); // 앱의 위치서비스 권한 확인 if (checkedPermission == LocationPermission.denied) { checkedPermission = await Geolocator.requestPermission(); if (checkedPermission == LocationPermission.denied) { return '위치 권한을 허가해주세요.'; } } if (checkedPermission == LocationPermission.deniedForever) { return '앱의 위치 권한을 세팅에서 허가해주세요.'; } return '위치 권한이 허가되었습니다.'; } AppBar renderAppBar() { return AppBar( title: Text( '오늘도 출근', style: TextStyle( color: Colors.blue, fontWeight: FontWeight.w700, ), ), backgroundColor: Colors.white, actions: [ IconButton( onPressed: () async { if (mapController == null) { return; } final location = await Geolocator.getCurrentPosition(); // 현재 위치 구하기 mapController!.animateCamera(CameraUpdate.newLatLng(LatLng( // 현재 위치로 카메라 이동 location.latitude, location.longitude, ))); }, color: Colors.blue, icon: Icon(Icons.my_location), ) ], ); } } class _CustomGoogleMap extends StatelessWidget { final CameraPosition initialPosition; final Circle circle; final Marker marker; final MapCreatedCallback onMapCreated; const _CustomGoogleMap({ required this.initialPosition, required this.circle, required this.marker, required this.onMapCreated, super.key, }); @override Widget build(BuildContext context) { return Expanded( flex: 2, child: GoogleMap( mapType: MapType.normal, // hybrid - 위성 지도 , normal - 일반 지도, terrain - 고저 지도 등 initialCameraPosition: initialPosition, myLocationEnabled: true, myLocationButtonEnabled: false, circles: Set.from([circle]), markers: Set.from([marker]), onMapCreated: onMapCreated, ), ); } } class _ChoolCheckButton extends StatelessWidget { final bool isWithinRange; final VoidCallback onPressed; final bool choolCheckDone; const _ChoolCheckButton({ required this.isWithinRange, required this.onPressed, required this.choolCheckDone, super.key, }); @override Widget build(BuildContext context) { return Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.timelapse_outlined, size: 50.0, color: choolCheckDone ? Colors.green : isWithinRange ? Colors.blue : Colors.red, ), const SizedBox(height: 20.0), if (!choolCheckDone && isWithinRange) TextButton( onPressed: onPressed, child: Text('출근하기'), ), ], ), ); } }
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';

class HomeScreen extends StatefulWidget {
  const HomeScreen({super.key});

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  bool choolCheckDone = false;
  GoogleMapController? mapController;

  // latitude 위도 , longitude 경도
  static final LatLng companyLatLng = LatLng(
    10.8007,
    106.7146,
  );

  static final CameraPosition initialPosition = CameraPosition(
    target: companyLatLng, // 최초 포지션
    zoom: 15, // zoom level
  );

  static final double okDistance = 100;
  static final Circle withInDistanceCircle = Circle(
    circleId: CircleId('withInDistanceCircle'),
    center: companyLatLng,
    fillColor: Colors.blue.withOpacity(0.5),
    radius: okDistance,
    strokeColor: Colors.blue,
    strokeWidth: 1,
  );
  static final Circle notWithInDistanceCircle = Circle(
    circleId: CircleId('notWithInDistanceCircle'),
    center: companyLatLng,
    fillColor: Colors.red.withOpacity(0.5),
    radius: okDistance,
    strokeColor: Colors.red,
    strokeWidth: 1,
  );
  static final Circle checkDoneCircle = Circle(
    circleId: CircleId('checkDoneCircle'),
    center: companyLatLng,
    fillColor: Colors.green.withOpacity(0.5),
    radius: okDistance,
    strokeColor: Colors.green,
    strokeWidth: 1,
  );

  static final Marker marker = Marker(
    markerId: MarkerId('marker'),
    position: companyLatLng,
  );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: renderAppBar(),
      body: FutureBuilder<String>(
        // FutureBuilder를 사용하여 리턴되는 값과 Future를 가져오는 함수의 상태에 따라 다른 UI를 렌더링 할수 있다.
        future: checkPermission(), // Future 를 리턴해주는 아무 함수나 넣을 수 있다.
        builder: (BuildContext context, AsyncSnapshot snapshot) {
          // future 에 넣은 함수의 리턴값은 snapshot에 들어간다.
          print(snapshot
              .connectionState); // none, waiting, active, done (future 함수의 진행상태)
          print(snapshot.data); // '위치 권한이 허가되었습니다'

          if (snapshot.connectionState == ConnectionState.waiting) {
            // 아직 사용자의 응답을 기다리고 있을때는 로딩바
            return Center(
              child: CircularProgressIndicator(),
            );
          }

          if (snapshot.data == '위치 권한이 허가되었습니다.') {
            return StreamBuilder<Position>(
                // FutureBuilder 와 같음
                stream: Geolocator.getPositionStream(),
                // position 이 변경될때마다 새로운 위치가 yield 되며, 제너릭 타입은 Position을 반환함 > build가 다시 실행됨
                builder: (context, snapshot) {
                  bool isWithinRange = false;

                  if (snapshot.hasData) {
                    final start = snapshot.data!;
                    final end = companyLatLng;
                    final distance = Geolocator.distanceBetween(
                      start.latitude,
                      start.longitude,
                      end.latitude,
                      end.longitude,
                    ); // 현재 거리와 회사 거리가 100m 이내인지 계산

                    if (distance < okDistance) {
                      isWithinRange = true;
                    }
                  }
                  return Column(
                    children: [
                      _CustomGoogleMap(
                        initialPosition: initialPosition,
                        circle: choolCheckDone
                            ? checkDoneCircle
                            : isWithinRange
                                ? withInDistanceCircle
                                : notWithInDistanceCircle,
                        marker: marker,
                        onMapCreated: onMapCreated,
                      ),
                      _ChoolCheckButton(
                        isWithinRange: isWithinRange,
                        choolCheckDone: choolCheckDone,
                        onPressed: onChoolCheckPressed,
                      ),
                    ],
                  );
                });
          }

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

  onMapCreated(GoogleMapController controller) {
    mapController = controller;
  }

  onChoolCheckPressed() async {
    final result = await showDialog(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          // alert dialog 를 쉽게 제작할 수 있음
          title: Text('출근하기'),
          content: Text('출근을 하시겠습니까?'),
          actions: [
            TextButton(
              onPressed: () {
                Navigator.of(context).pop(
                    false); // dialog 를 하나의 스크린처럼 생각하면됨. 스크린 전환에서 뒤돌아가기 처럼 pop()
              },
              child: Text('취소'),
            ),
            TextButton(
              onPressed: () {
                Navigator.of(context).pop(true);
              },
              child: Text('출근하기'),
            )
          ],
        );
      },
    );

    if (result) {
      setState(() {
        choolCheckDone = true;
      });
    }
  }

  Future<String> checkPermission() async {
    // 권한 관련 작업은 모두 async로 한다. 유저가 누를때까지 기다려야하기 때문
    final isLocationEnabled = await Geolocator
        .isLocationServiceEnabled(); // 해당 기기가 위치 서비스를 사용중인지 확인 (앱이 아닌 디바이스 자체의 위치서비스)

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

    /*
    * LocationPermission 타입
    * denied : default >> Geolocator.requestPermission() 로 권한 요청 하면됨
    * deniedForever : 권한을 허가하지 않은 것 >> 다시 앱이 권한을 요청할 수 없음 (유저가 직접 설정으로 가서 권한을 열도록 유도해야함)
    * whileInUse : 앱 실행중에만 권한 허가
    * always : 항상 허가
    * */

    LocationPermission checkedPermission =
        await Geolocator.checkPermission(); // 앱의 위치서비스 권한 확인
    if (checkedPermission == LocationPermission.denied) {
      checkedPermission = await Geolocator.requestPermission();

      if (checkedPermission == LocationPermission.denied) {
        return '위치 권한을 허가해주세요.';
      }
    }

    if (checkedPermission == LocationPermission.deniedForever) {
      return '앱의 위치 권한을 세팅에서 허가해주세요.';
    }

    return '위치 권한이 허가되었습니다.';
  }

  AppBar renderAppBar() {
    return AppBar(
      title: Text(
        '오늘도 출근',
        style: TextStyle(
          color: Colors.blue,
          fontWeight: FontWeight.w700,
        ),
      ),
      backgroundColor: Colors.white,
      actions: [
        IconButton(
          onPressed: () async {
            if (mapController == null) {
              return;
            }

            final location = await Geolocator.getCurrentPosition(); // 현재 위치 구하기

            mapController!.animateCamera(CameraUpdate.newLatLng(LatLng( // 현재 위치로 카메라 이동
              location.latitude,
              location.longitude,
            )));
          },
          color: Colors.blue,
          icon: Icon(Icons.my_location),
        )
      ],
    );
  }
}

class _CustomGoogleMap extends StatelessWidget {
  final CameraPosition initialPosition;
  final Circle circle;
  final Marker marker;
  final MapCreatedCallback onMapCreated;

  const _CustomGoogleMap({
    required this.initialPosition,
    required this.circle,
    required this.marker,
    required this.onMapCreated,
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    return Expanded(
      flex: 2,
      child: GoogleMap(
        mapType: MapType.normal,
        // hybrid - 위성 지도 , normal - 일반 지도, terrain - 고저 지도 등
        initialCameraPosition: initialPosition,
        myLocationEnabled: true,
        myLocationButtonEnabled: false,
        circles: Set.from([circle]),
        markers: Set.from([marker]),
        onMapCreated: onMapCreated,
      ),
    );
  }
}

class _ChoolCheckButton extends StatelessWidget {
  final bool isWithinRange;
  final VoidCallback onPressed;
  final bool choolCheckDone;

  const _ChoolCheckButton({
    required this.isWithinRange,
    required this.onPressed,
    required this.choolCheckDone,
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    return Expanded(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(
            Icons.timelapse_outlined,
            size: 50.0,
            color: choolCheckDone
                ? Colors.green
                : isWithinRange
                    ? Colors.blue
                    : Colors.red,
          ),
          const SizedBox(height: 20.0),
          if (!choolCheckDone && isWithinRange)
            TextButton(
              onPressed: onPressed,
              child: Text('출근하기'),
            ),
        ],
      ),
    );
  }
}