Flutter에서 기기 등록을 구현해보겠습니다. 저희 프로젝트의 주제는 "비가청 주파수를 이용한 출석체크 앱"으로 학생들이 1인당 1개의 기기로만 출석체크를 할 수 있도록 기기등록 기능을 구현하는 것이 중요합니다.
먼저 기기등록 화면을 만들어보겠습니다.
device.dart
class Device extends StatefulWidget {
_DeviceScreenState createState() => _DeviceScreenState();
}
class _DeviceScreenState extends State<Device> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('기기등록'),
centerTitle: true,
backgroundColor: Color(0xff8685A6),
actions: [
IconButton(
icon: Icon(Icons.settings),
onPressed: () {},
)
],
),
body: Padding(
//두번째 padding <- LIstview에 속함.
padding: const EdgeInsets.all(16.0),
child: SingleChildScrollView(
child: Form(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
SizedBox(height: 15),
Text("현재 접속한 기기를 등록하시겠습니까?"),
SizedBox(height: 5),
Text(
"출석체크를 위해서는 기기를 등록 하셔야 합니다.\n등록한 기기는 2주 이내에 변경이 불가능합니다."),
Text(
'현재 접속 기기 : ',
style: TextStyle(
color: Color(0xff8685A6),
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 10),
NextButton(text: "기기등록", onpress: () {}),
SizedBox(height: 15),
Container(
height: 1.0, width: 500.0, color: Color(0xffD8DADC)),
SizedBox(height: 15),
Text("현재 등록 기기"),
],
),
),
),
));
}
}
완성된 화면의 모습입니다.

이제 기기등록을 구현하기 위해서 프론트엔드에서 기기 이름, 기기 고유 번호를 얻어야 합니다. 기기 정보를 가져오기 위해서 device_info 패키지를 사용해 보겠습니다.
pubspec.yaml 파일에 이 코드를 추가합니다.
dependencies:
device_info: ^2.0.3
terminal에서 이 코드를 실행해주면 패키지가 설치됩니다.
flutter pub get
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:device_info/device_info.dart';
import 'package:flutter/material.dart';
기기 정보를 가져오기 위해서 device_info 패키지를 import 합니다. 또한, Platform 정보를 가져오기 위해 dart:io 패키지와 flutter/services.dart 패키지도 import 해줍니다.
안드로이드와 ios의 경우 각각 다음과 같이 기기 고유 번호와 기기 모델명을 가져올 수 있습니다.
안드로이드 경우:
final AndroidDeviceInfo androidInfo = await deviceInfoPlugin.androidInfo;
device_id = androidInfo.androidId; // 안드로이드 기기 고유 번호
device_type = androidInfo.model; //안드로이드 기기 모델명
ios 경우:
final IosDeviceInfo iosInfo = await deviceInfoPlugin.iosInfo;
device_id = iosInfo.identifierForVendor; // ios 기기 고유 번호
device_type = iosInfo.model; //ios 기기 모델명
기기 정보를 가져오는 비동기 함수 'getDeviceInfo()'를 만들어줍니다.
device_info.dart
Future<Map<String, String>> getDeviceInfo() async {
Map<String, dynamic> deviceData = <String, dynamic>{};
final DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin();
String device_id = ''; //기기 고유번호
String device_type = ''; //기기 종류
try {
if (Platform.isAndroid) { //안드로이드
final AndroidDeviceInfo androidInfo = await deviceInfoPlugin.androidInfo;
device_id = androidInfo.androidId; // 안드로이드 기기 고유 번호
device_type = androidInfo.model;
} else if (Platform.isIOS) { //ios
final IosDeviceInfo iosInfo = await deviceInfoPlugin.iosInfo;
device_id = iosInfo.identifierForVendor; // iOS 기기 고유 번호
device_type = iosInfo.model;
}
} on PlatformException { //예외 발생
device_id = '';
device_type = '';
}
return {'device_type': device_type, 'device_id': device_id};
}
getDeviceInfo() 함수는 deviceData라는 Map을 선언해 device_type과 device_id를 묶어서 반환해줍니다.
이제 아까 만든 화면에서 getDeviceInfo() 함수를 호출하여 화면에 표시해주겠습니다.
// ...
Map<String, String> deviceInfo = {}; //기기 정보를 저장할 빈 Map을 선언
void initState() {
super.initState();
_getDeviceInfo();
}
Future<void> _getDeviceInfo() async {
deviceInfo = await getDeviceInfo();
setState(() {});
}
// ...
Text(
'현재 접속 기기 : ${deviceInfo['device_type'] ?? ''}',
style: TextStyle(
color: Color(0xff8685A6),
fontWeight: FontWeight.bold,
),
),
// ...
NextButton(
text: "기기등록",
onpress: () {
print(deviceInfo['device_id']);
}),
// ...
_getDeviceInfo()는 비동기 함수로, getDeviceInfo() 함수를 호출하여 기기 정보를 가져오고 deviceInfo 변수에 할당합니다. deviceInfo 변수에 값이 할당된 후에는 setState()를 호출하여 deviceInfo의 값이 변경되었음을 Flutter에 알리고 화면이 업데이트되어 기기 정보가 표시됩니다.
이렇게 _getDeviceInfo() 함수를 사용하여 기기 정보를 가져오고 화면을 업데이트하는 방식으로, 기기 정보를 동적으로 표시하는 기능이 구현되었습니다.
이제 받을 기기 정보를 백엔드에 보내고 API를 연결하여 서버와 통신하는 과정이 필요합니다.
flutter에서 API 통신을 위해 가장 일반적으로 사용되는 패키지는 http 패키지입니다. http 패키지는 HTTP 요청을 생성하고 서버로 보내는 기능을 제공합니다.
pubspec.yaml 파일에 다음 내용을 추가해주고 terminal에서 flutter pub get 명령을 실행하여 패키지를 설치합니다.
dependencies:
http: ^0.13.3
다운로드한 http 패키지와 JSON 데이터 처리를 위한 dart:convert 패키지를 import 해줍니다.
import 'package:http/http.dart' as http;
import 'dart:convert';
deviceType과 deviceSerial을 사용하여 새로운 기기를 등록하기 위해 DRF API로 POST 요청을 보내는 addDevice 비동기 함수를 작성합니다.
add_device.dart
import 'package:http/http.dart' as http;
import 'dart:convert';
Future<http.Response> addDevice(String deviceType, String deviceSerial) async {
final url = Uri.parse('<http://http>://localhost:8000/serial/save-device/'); //url 변수에 DRF API의 엔드포인트 URL 할당
final response = await http.post( //http.post() 메서드를 사용해 post 요청 보냄
url,
headers: {'Content-Type': 'application/json'},
body: jsonEncode({ //jsonEncode() 함수를 사용하여 deviceType과 deviceSerial을 JSON 형식으로 변환한 후 전달
'device_name': deviceType,
'device_serial': deviceSerial,
}),
);
return response;
}
addDevice() 함수에서는 http.post() 메서드를 사용하여 POST 요청을 보내고 응답을 response 변수에 반환합니다. 이 함수를 호출해서 새로운 기기를 등록하는 API 요청을 수행할 수 있습니다.
API 요청을 수행하는 함수를 만들기 전 기기등록 성공/실패 여부를 알려주는 사용자 지정 대화상자 DialogFormat 클래스를 만들어줍니다.
import 'package:flutter/material.dart';
class DialogFormat {
static void customDialog({
required BuildContext context,
required String title,
required String content,
}) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(title),
content: Text(content),
actions: <Widget>[
TextButton(
child: Text('OK'),
onPressed: () => Navigator.of(context).pop(),
),
],
),
);
}
}
이제 addDevice() 함수를 호출하여 API 요청을 수행하는 함수 _registerDevice() 함수를 만들어 줍니다.
Future<void> registerDevice() async {
final deviceType = deviceInfo['device_type'];
final deviceSerial = deviceInfo['device_id'];
final student_id = "2300000";
final response = await addDevice(
student_id,
deviceType!,
deviceSerial!,
);
if (response.statusCode == 200) {
// 기기 등록 성공
print('기기 등록에 성공했습니다.');
DialogFormat.customDialog(
context: context,
title: 'Success',
content: '기기 등록에 성공했습니다.',
);
} else {
// 기기 등록 실패
print('기기 등록에 실패했습니다.');
DialogFormat.customDialog(
context: context,
title: 'Error',
content: '기기 등록에 실패했습니다.',
);
}
}
이제 연결이 완료되었으니 API 통신이 되는지 확인해 봅시다.
기기 등록 버튼을 눌렀지만 다음과 같은 에러가 발생하였습니다.

우리 프로젝트의 db, drf 서버는 현재 docker 컨테이너이기 때문에 localhost 대신 ip 주소 사용해 보았습니다. 도커 컨테이너의 IP 주소를 확인하고, Flutter 애플리케이션에서 해당 IP 주소를 사용하여 API에 연결해야 합니다.
powershell 에서 다음 명령어를 실행합니다.
docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' [컨테이너명 또는 컨테이너ID]

django 컨테이너의 ip 주소는 172.19.0.3입니다.
final url = Uri.parse('http://http://172.19.0.3:8000/serial/save-device/');
다시한번 기기등록을 시도해 보았으나 서버로부터 응답이 돌아오지 않았습니다.
서버에 접근 가능한 호스트가 제한되어 있는지 확인합니다.

Django의 settings.py 파일을 확인해 보았지만 모든 호스트가 허용되어 있었습니다.
Flutter 앱이 Android 에뮬레이터에서 실행 중이라면, Docker 컨테이너 IP 대신 10.0.2.2를 사용해 호스트 컴퓨터에 접근할 수 있습니다. 이는 Android 에뮬레이터가 가상 네트워크를 통해 호스트 시스템과 통신하기 때문입니다.
저는 Android 에뮬레이터를 사용 중이었기 때문에 이 방법 또한 시도해 보았습니다.
final url = Uri.parse('http://10.0.2.2:8000/serial/save-device/');
기기 등록을 시도한 결과 성공적으로 서버와 통신하여 db에 기기명, 기기 고유 번호가 저장되었습니다.


mysql-server의 terminal에서 serial_device table에 select를 해본 결과 db에도 device_name, device_serial이 성공적으로 저장된 것을 확인할 수 있습니다.


Django_server container에도 Log가 정상적으로 뜨는 것을 확인하였습니다.

하지만 또 다른 문제가 있습니다. 기기 등록을 완료한 후 다시 기기등록 버튼을 눌렀을 때는 한 사람이 중복으로 기기를 저장하려는 것이기 때문에 기기 등록을 성공해서는 안됩니다. 하지만, 계속 성공 showlog가 뜨는 문제가 발생했습니다.
문제의 원인을 알기 위해 서버의 api에 직접 POST 요청을 해본 결과 이미 등록된 디바이스를 저장하지 않고 error status로 정상적으로 출력하는 것을 알 수 있습니다.

또한, 해당 API의 views.py를 살펴보면 응답의 상태를 statuscode로 나타내는 대신에 status와 message 필드를 JSON 형식으로 보내어 상태를 표시하고 있는 것을 알 수 있습니다.

그러므로 response.statusCode 대신 response.body의 status와 message로 상태를 구분하는 코드로 변경해줍니다.
Future<void> registerDevice() async {
final deviceType = deviceInfo['device_type'];
final deviceSerial = deviceInfo['device_id'];
final student_id = "2300000";
final response = await addDevice(
student_id,
deviceType!,
deviceSerial!,
);
final responseData = jsonDecode(response.body);
final status = responseData['status'];
if (status == 'error') {
final message = responseData['message'];
if (message == '이미 등록된 디바이스입니다.') {
//print('이미 등록된 디바이스입니다.');
DialogFormat.customDialog(
context: context,
title: 'Error',
content: '이미 등록된 디바이스입니다.',
);
} else {
//print('기기 등록에 실패했습니다.');
DialogFormat.customDialog(
context: context,
title: 'Error',
content: '기기 등록에 실패했습니다.',
);
}
} else {
//print('기기 등록에 성공했습니다.');
DialogFormat.customDialog(
context: context,
title: 'Success',
content: '기기 등록에 성공했습니다.',
);
}
}
이제 한번 등록한 디바이스에서 Error 팝업창을 정상적으로 띄우는 것을 확인할 수 있습니다.


이렇게 새로운 device를 등록하는 코드를 구현해보았습니다. 이제 등록된 device가 있을 경우 현재 등록 기기에 띄우는 코드를 작성해보겠습니다.
models.py

views.py의 get_device 함수의 코드입니다. url은 /serial/get-device/이고 student_id를 post해야 한다는 것을 알 수 있습니다.

Future<http.Response> getCurrentDeviceInfo(String student_id) async {
final url = Uri.parse('http://10.0.2.2:8000/serial/get-device/');
final response = await http.post(
url,
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'student_id': student_id,
}),
);
return response;
}
student-id를 Json 형식으로 보내 서버에 응답을 요청하는 getCurrentDeviceInfo 비동기 함수를 작성합니다.
String current_id = "";
String current_name = "";
String current_time = "";
//...
Future<void> getCurrentDevice() async {
final response = await getDevice(
student_id,
);
final responseData = jsonDecode(response.body);
//print(responseData);
final status = responseData['status'];
if (status == 'success') {
print('등록된 디바이스가 있습니다.');
current_id = responseData['device_id'];
current_name = responseData['device_name'];
current_time = responseData['timestamp'];
} else {
throw Exception('등록된 디바이스가 없습니다.');
}
}
current_id != ""
? Container(
height: 80,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10.0),
border: Border.all(
width: 3,
color: Color(0xff8685A6),
),
),
child: Row(
children: [
Icon(Icons.smartphone, // 왼쪽에 표시할 아이콘
color: Colors.black,
size: 50.0),
SizedBox(width: 10.0), // 아이콘과 글씨 사이의 간격 조정
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"${current_name}-${current_id}", // 첫 번째 줄로 표시할 글씨
style: TextStyle(
color: Colors.black,
fontSize: 20.0, // 첫 번째 줄 글씨 크기 조정
// 진한 글씨체 적용
),
),
Text(
current_time, // 두 번째 줄로 표시할 글씨
style: TextStyle(
color: Colors.grey, // 연한 색상 적용
fontSize: 14.0, // 두 번째 줄 글씨 크기 조정
),
),
],
),
),
],
),
)
: Text("등록된 기기가 없습니다.")
status가 success일 때 이미 해당 student_id인 학생이 이미 등록된 device가 있어 현재 등록된 device의 정보를 response.body에 담아 보내주는 것이기 때문에 String current_id, current_name, current_time에 해당 값들을 담아줍니다.
이제 테스트용 버튼의 onPress에 getCurrentDevice()함수를 넣어주고 API 통신이 되는지 확인해 봅시다.
다음과 같은 에러가 발생했습니다.

이 에러의 의미는 Dart/Flutter 코드에서 'int' 타입이 'String' 타입의 서브타입(subtype)이 아니라는 의미입니다. 하지만 우리가 받아야하는 device_id, device_name, timestamp는 모두 string type입니다.
에러의 원인을 찾기 위해 서버의 api에도 직접 POST 요청을 해보았습니다. 현재 기기 고유 번호를 담아야하는 device_id가 int 형태인데, 기기 고유 번호가 아닌 db에서의 id를 받은 것을 확인할 수 있습니다.

django 서버의 views.py에서 'device_id': recent_device.id를 'device_id': recent_device.device_serial로 변경해주고 다시 기기등록을 실행했습니다.

이제 DEBUG CONSOLE에는 등록한 디바이스가 있다고 뜨지만

화면에는 코드가 바로 적용되지 않습니다.

이를 해결하기 위해 getCurrentDevice() 함수 내에서 setState() 함수를 호출하여 current_id, current_name, current_time 변수들의 값을 업데이트하였습니다.
if (status == 'success') {
print('등록된 디바이스가 있습니다.');
setState(() { //setState 함수 추가
current_id = responseData['device_id'];
current_name = responseData['device_name'];
current_time = responseData['timestamp'];
});
print(current_id);
} else {
throw Exception('등록된 디바이스가 없습니다.');
}
이렇게 하니 변경된 값들이 화면에 바로 반영되는 것을 볼 수 있습니다.

이렇게 기기등록 기능을 완성해보았습니다.
