- 주요 스택
- Google Maps 지도
- 지도 마커
- 지도 동그라미 표시
- 현재 위치 표시 및 위도/경도 구하기
- 위도, 경도간 거리 구하기
지도 띄워보기
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 추가, 상 하단 분리
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 앱 권한 체크 및 허용 요청
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 상태 관리
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( '출근', ), ); } }
지도에 원 그리기
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 과 완전 비슷함
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 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 로 출근 여부 받기 / 상태에 따라 원 변화시키기
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; 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('출근하기'), ), ], ), ); } }