네비게이션바(임시) | 시작화면 | 로그인 |
---|---|---|
회원가입 | 로딩화면 | 메인 화면 (대여 전) |
---|---|---|
QR 스캔 | 오류보고 | 메뉴 |
---|---|---|
사용자 프로필 | 결제 정보 | 이용 방법 | 문의 내역 |
---|---|---|---|
추후 작업 요망
- 챗봇 기능 추가 시 챗봇 위젯 구현 필요
- 위젯간 데이터 전송 및 상태 관리
- API 를 통한 request 및 response 처리
에러 발생
INFO: 172.18.0.1:50394 - "POST /users/login HTTP/1.1" 422 Unprocessable Entity
만들어놓은 레이아웃에 적용시킨 소스 코드
import 'dart:convert';import 'package:dio/dio.dart';import 'package:flutter/material.dart';import 'package:flutter_project/const/constants.dart';import 'package:flutter_project/screen/1_start.dart';import 'package:flutter_project/screen/3_signup.dart';import 'package:flutter_project/widgets/basic_button.dart';import 'package:flutter_project/widgets/input_box.dart';import 'package:flutter_project/widgets/logo.dart';class Login extends StatefulWidget {
Login({super.key}); @override State<Login> createState() => _LoginState();}
class _LoginState extends State<Login> {
String user_email = ''; String password = ''; bool logoAppear = true; @override Widget build(BuildContext context) {
final bottomInset = MediaQuery.of(context).viewInsets.bottom; return Scaffold(
appBar: AppBar(
title: Text("로그인"), ), backgroundColor: MAIN_COLOR, body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10.0), child: Container(
width: MediaQuery.of(context).size.width, child: Column(
mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, children: [
showLogo(), InputBox(
hintText: "아이디", onTap: () {
setState(() {
logoAppear = false; }); }, onChanged: (String value) {
user_email = value; }, ), SizedBox(
height: 10.0, ), InputBox(
hintText: "비밀번호", onTap: () {
setState(() {
logoAppear = false; }); }, onChanged: (String value) {
password = value; }, ), Row(
children: [
Checkbox(value: false, onChanged: (value) {}), Text('자동 로그인'), ], ), SizedBox(
height: 30.0, ), Row(
mainAxisAlignment: MainAxisAlignment.center, children: [
Expanded(
child: BasicButton(
buttonTitle: "로그인", onPressed: () {
postLoginInfo(); print('##### 토큰 정보 : ${postLoginInfo().toString()} #####'); }, ), ), SizedBox(
width: 10.0, ), Expanded(
child: BasicButton(
buttonTitle: "계정 찾기", onPressed: () => Navigator.push(
context, MaterialPageRoute(
builder: (context) => Start(), ), ), ), ), ], ), BasicButton(
buttonTitle: "회원가입", onPressed: () => Navigator.push(
context, MaterialPageRoute(
builder: (context) => SignUp(), ), ), ), ], ), ), ), ); }
Widget showLogo() {
if (logoAppear) {
return Column(
children: [
Logo(), SizedBox(
height: 50.0, ), ], ); } else {
return SizedBox(
height: 20.0, ); }
}
Future<Map> postLoginInfo() async {
Map loginInfo = {
'user_email': user_email, 'password': password, }; print('##### loginInfoMap is ${loginInfo} #####'); final response = await Dio().post(
'http://10.0.2.2:8000/users/login', data: jsonEncode(loginInfo), ); return response.data; }
}
2023.02.28. 로그인, 로그아웃 구현 마무리 작업
- TextFormField 위젯의 onChanged 함수를 이용하면 이메일과 비밀번호를 작성하는 중에도 상태가 변경되어 상태관리가 어려워진다.
- 대안으로 onSaved 함수를 이용해 값을 저장하고 TextFormField 위젯을 Form 위젯으로 감싸 GlobalKey() 클래스를 이용해 저장된 값들을 관리한다.
- Dio를 이용해 응답받은 토큰은 FlutterSecureStorage 라이브러리를 이용해 값을 저장, 로그아웃에 이용한다.
- showDialog함수를 이용해 로그인 성공, 실패시 알림창이 뜨게하고 알림창을 확인했을 때 Navigator를 이용해 알맞은 스크린으로 이동하도록 한다.
- obscureText 기능을 통해 비밀번호 입력 시 “*”기호로 보이게 한다.
소스코드 | 화면 |
---|---|
소스코드 | 화면 |
---|---|
소스코드 | 화면 |
---|---|
flutter_naver_map 플러그인을 통해 지도 위에 우산 대여함의 위치를 마킹하고 우산 대여함 아이콘을 클릭하면 우산 대여 화면으로 넘어가도록 한다.
네이버 API 사용 방법
- 네이버 클라우드 콘솔에 접속해 지도 API Key를 발급받는다.
- 현재 진행 중인 플러터 프로젝트 명을 그대로 입력한다.
initialCameraPosition
키를 통해 지도를 켰을 때 처음 위치를 설정한다.markers
키를 통해 마커를 입력한다.Marker(
markerId: '1',
position: LatLng(standLatitude, standLongitude),
icon: snapshot.data,
width: 32,
height: 32,
onMarkerTab: (marker, iconSize) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text("우산 대여 화면으로\n이동하시겠습니까?"),
content: Row(
mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) =>
RentScreen(),
),
);
},
child: Text("예"),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text("아니오"),
),
],
),
);
},
);
},
),
dependencies:
geolocator: ^9.0.2
import 'package:geolocator/geolocator.dart';
Future<String> checkLocationPermission() async {
final isLocationEnabled = await Geolocator.isLocationServiceEnabled();
if (!isLocationEnabled) {
return "위치 서비스를 활성화해주세요.";
}
LocationPermission checkedPermission = await Geolocator.checkPermission();
if (checkedPermission == LocationPermission.denied) {
checkedPermission = await Geolocator.requestPermission();
if (checkedPermission == LocationPermission.denied) {
return "위치 권한을 허가해주세요.";
}
}
if (checkedPermission == LocationPermission.deniedForever) {
return "앱의 위치 권한을 설정에서 허가해주세요.";
}
return "위치 권한이 허가 되었습니다.";
}
class BeforeRentScreen extends StatefulWidget {
const BeforeRentScreen({Key? key}) : super(key: key);
@override
State<BeforeRentScreen> createState() => _BeforeRentScreenState();
}
class _BeforeRentScreenState extends State<BeforeRentScreen> {
final standLatitude = 37.5706;
final standLongitude = 126.9853;
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () {
return Future(() => false);
},
child: Scaffold(
appBar: AppBar(
title: Text("Before Rent Screen"),
leading: Container(),
// 배포 시 홈버튼 삭제
actions: [homeButton(context)],
),
body: Stack(
children: [
FutureBuilder<String>(
future: checkLocationPermission(),
builder: (context, snapshot) {
if (snapshot.hasData &&
snapshot.connectionState == ConnectionState.waiting) {
return Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.data == '위치 권한이 허가 되었습니다.') {
return FutureBuilder(
future: OverlayImage.fromAssetImage(
assetName: 'assets/images/umb_icon.png',
),
builder: (context, snapshot) {
return NaverMap( ...
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
void main() {
runApp(
MaterialApp(
home: HomeScreen(),
),
);
}
class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key);
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
String? imagePath;
XFile? tokenPthoto;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
ElevatedButton(
onPressed: () async {
final image =
await ImagePicker().pickImage(source: ImageSource.gallery);
print(image);
},
child: Text("Load Image"),
),
ElevatedButton(
onPressed: () async {
final image = await ImagePicker().pickImage(
source: ImageSource.camera,
maxHeight: 100,
maxWidth: 100,
imageQuality: 30,
);
print("이미지 경로 : ${image!.path}");
print("이미지 경로 타입 : ${image.path.runtimeType}");
setState(() {
imagePath = image.path;
tokenPthoto = image;
});
},
child: Text("Take a Photo"),
),
previewPhoto(),
],
),
);
}
Widget previewPhoto() {
if (imagePath == null) {
return Container(
child: Text("이미지 경로 없음"),
);
} else {
return Column(
children: [
Text("이미지 경로 : $imagePath"),
Image.file(
File(imagePath!),
width: 200,
height: 200,
),
],
);
}
}
}
첫 화면 | 이미지 선택 화면 | 이미지 로드 완료 |
---|---|---|
챗봇 응답 서버와 통신 가능 테스트
챗봇 응답 서버와 통신 가능 테스트
http 패키지의 post 메서드를 이용해 서버의 챗봇 인공지능 모델과 통신한다.
ListView builder를 이용해 사용자와 챗봇의 메세지를 화면에 나타낸다.
소스코드 :
dependencies:
qr_code_scanner: ^1.0.1
dependencies:
screenshot: ^1.3.0
class RentScreen extends StatefulWidget {
const RentScreen({Key? key}) : super(key: key);
@override
State<RentScreen> createState() => _RentScreenState();
}
class _RentScreenState extends State<RentScreen> {
final screenshotController = ScreenshotController();
String? qrCode;
QRViewController? controller;
final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
bool removeQrScannerOverlayShape = false;
@override
Widget build(BuildContext context) {
double deviceWidth = MediaQuery.of(context).size.width;
return Scaffold( ... 생략 ...
... 생략 ...
return Scaffold(
appBar: AppBar(
title: Text("Rent Screen"),
actions: [
homeButton(context)
],
leading: IconButton(
icon: Icon(Icons.arrow_back),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => BeforeRentScreen(),
),
);
},
),
),
body: Screenshot(
controller: screenshotController,
child: QRView(
key: qrKey,
overlay: renderQrScannerOverlayShape(deviceWidth),
onQRViewCreated: (controller) {
controller.scannedDataStream.listen(
(scanData) {
if (scanData.code == null) {
return;
} else {
qrCode = scanData.code;
setState(
() {
removeQrScannerOverlayShape = true;
},
);
controller.pauseCamera().then(
(_) async {
final capturedImg = await screenshotController.capture();
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Image.memory(
capturedImg!,
width: deviceWidth * 0.75,
height: deviceWidth * 0.75,
),
actions: [
ElevatedButton(
onPressed: () {
setState(
() {
removeQrScannerOverlayShape = false;
},
);
Navigator.pop(context);
controller.resumeCamera();
},
child: Text("다시 스캔"),
),
FutureBuilder(
future:
FlutterSecureStorage().read(key: "token"),
builder: (context, snapshot) {
return ElevatedButton(
child: Text("대여"),
onPressed: () async {
await Dio()
.post(
'$API_DOMAIN/flutter/rent',
data: jsonEncode(
{
"token": snapshot.data,
"qr_code": qrCode,
},
),
)
.then(
(value) {
if (value.data['msg'] == null) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title:
Text("서버와 통신에 실패하였습니다"),
actions: [
ElevatedButton(
child: Text("확인"),
onPressed: () {
Navigator.of(context)
.pop();
},
),
],
);
},
);
} else if (value.data['msg'] ==
"대여 성공") {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text("대여 성공했습니다"),
actions: [
ElevatedButton(
child: Text("확인"),
onPressed: () {
Navigator.of(context)
.push(
MaterialPageRoute(
builder: (context) =>
AfterRentScreen(),
),
);
},
),
],
);
},
);
} else if (value.data['msg'] ==
"대여 실패") {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text("대여 실패했습니다"),
actions: <Widget>[
ElevatedButton(
child: Text("확인"),
onPressed: () {
Navigator.of(context)
.push(
MaterialPageRoute(
builder: (context) =>
RentScreen(),
),
);
},
),
], // add this
);
},
);
}
},
);
},
);
},
),
],
);
},
);
},
);
}
},
);
},
),
),
);
renderQrScannerOverlayShape
을 추가... 생략 ...
QrScannerOverlayShape renderQrScannerOverlayShape(deviceWidth) {
if (removeQrScannerOverlayShape == false) {
return QrScannerOverlayShape(
borderColor: Colors.red,
borderRadius: 5,
borderLength: 15,
borderWidth: 5,
cutOutSize: deviceWidth * 0.2,
);
} else {
return QrScannerOverlayShape(
overlayColor: Colors.black.withOpacity(0.0),
);
}
}
소스코드
import 'dart:convert'; import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:qr_code_scanner/qr_code_scanner.dart'; import 'package:screenshot/screenshot.dart'; import '../const/constant.dart'; class ReturnScreen extends StatefulWidget { const ReturnScreen({Key? key}) : super(key: key); @override State<ReturnScreen> createState() => _ReturnScreenState(); } class _ReturnScreenState extends State<ReturnScreen> { final screenshotController = ScreenshotController(); String? qrCode; QRViewController? controller; final GlobalKey qrKey = GlobalKey(debugLabel: 'QR'); bool removeQrScannerOverlayShape = false; @override Widget build(BuildContext context) { double deviceWidth = MediaQuery.of(context).size.width; return Scaffold( appBar: AppBar( title: Text("Return Screen"), ), body: Screenshot( controller: screenshotController, child: QRView( key: qrKey, overlay: renderQrScannerOverlayShape(deviceWidth), onQRViewCreated: (controller) { controller.scannedDataStream.listen( (scanData) { if (scanData.code == null) { return; } else { qrCode = scanData.code; setState(() { removeQrScannerOverlayShape = true; }); controller.pauseCamera().then( (_) async { final capturedImg = await screenshotController.capture( pixelRatio: 0.50, ); showDialog( context: context, builder: (context) { return AlertDialog( title: Column( children: [ Image.memory(capturedImg!, width: deviceWidth * 0.75, height: deviceWidth * 0.75), Text(qrCode!), Row( children: [ ElevatedButton( onPressed: () { setState( () { removeQrScannerOverlayShape = false; }, ); Navigator.pop(context); controller.resumeCamera(); }, child: Text("다시 스캔"), ), FutureBuilder( future: FlutterSecureStorage() .read(key: "token"), builder: (context, snapshot) { return ElevatedButton( onPressed: () async { var response = await Dio().post( '$API_DOMAIN/flutter/return', data: jsonEncode( { "token": snapshot.data, "qr_code": qrCode, "image": capturedImg, }, ), ); print( "response : ${response.toString()}"); Navigator.of(context).pop(); }, child: Text("반납"), ); }, ), ], ), ], ), ); }, ); }, ); } }, ); }, ), ), ); } QrScannerOverlayShape renderQrScannerOverlayShape(deviceWidth) { if (removeQrScannerOverlayShape == false) { return QrScannerOverlayShape( borderColor: Colors.red, borderRadius: 5, borderLength: 15, borderWidth: 5, cutOutSize: deviceWidth * 0.2, ); } else { return QrScannerOverlayShape( overlayColor: Colors.black.withOpacity(0.0), ); } } }
구현된 화면
결과
대안