2023. 11. 4. 17:39ㆍ모바일어플개발/Flutter 실전어플 개발
안녕하세요~ totally 개발자입니다.
이 포스팅에서는 지난 시간에 이어 결제수단 옵션을 선택하고 간단한 양식 검증(비어 있는지 비밀번호가 일치하는 지 정도만 체크)해서 주문완료 페이지로 이동할 수 있도록 진행합니다.
지난 포스팅까지 진행하신 분들이라면 이 포스팅에서는 변경되는 부분이 많기에 잘 확인하시면서 따라오셔야 합니다.
Step 1: item_checkout_page.dart에 아래 변수들을 선언해줍니다. 있는 경우에는 넘어가시면 됩니다.

Step 2: 지난 포스팅을 하신 분들이라면 여기에서 잘 변경을 해주셔야 하는데 기존에 입력하는 TextFormField를 우편번호 부분을 제외하고 하나로 통합하기 위해 inputTextField 위젯을 별도로 만들어주고 다음처럼 설정을 해줍니다. 파라미터들을 통해 각각 매개변수를 넘겨주고 validator를 통해 간단한 빈 칸 검증 및 비밀번호인 경우 일치하는지만 체크해주었습니다. (자세한 설명은 유튜브 강좌에 언급합니다)

Step 3: 결제수단 옵션 Dropdown 버튼 위젯을 만들어 줍니다. 위에 선언한 변수들을 사용하여 구성할 수 있으며 items 부분에 기존 리스트 변수와 map를 활용하여 DropdownMenuItem 위젯을 쭉 생성해주면 됩니다.

Step 4: 지난 포스팅까지 따라오신 분들께서는 이 부분에서 많은 수정이 들어갑니다. 기존 위젯들 중 receiverZipTextField를 제외한 부분을 모두 제거하고 inputTextField에 매개변수만 달리 해서 추가해주시면 되고 카드결제를 선택했는지, 무통장입금을 선택했는지 여부에 따라 위젯을 다르게 보여줄 수 있도록 구성합니다. (반드시 이렇게 해야 하는 것은 아닙니다. 각각 실무 환경에 따라 적절한 방법을 선택해서 사용하시면 됩니다) 또한 Form 위젯과 formKey를 key값으로 넣어주시면 됩니다. 전체 소스 코드는 맨 아래에 있습니다.

Step 5: 제가 inputTextField는 item_checkput_page.dart에서 별도로 분리했지만 실제 component로 이렇게 구성해서 분리하는 것을 더 권장을 드리고 있습니다. 그래서 Dialog 경우에는 components 폴더를 만들고 그 안에 basic_dialog.dart 파일로 별도로 생성해서 만들어주었습니다. 모든 입력 내용을 입력했으나 결제수단을 선택하지 않을 때 이 Dialog가 나오게 하기 위함입니다.

Step 6: 결제 버튼 클릭했을 때 이동되는 item_order_result_page.dart를 만들어주고 틀만 보여질 수 있게만 구성합니다. 다음 포스팅에 주문완료 페이지를 제대로 구성할 예정입니다.

Step 7: 다시 item_checkout_page.dart로 가셔서 이 부분들을 import 해주세요.

Step 8: 마지막으로 결제하기 버튼을 눌렀을 때 비밀번호 일치와 빈 칸이 아닌지를 검증하고 결제수단선택이 카드결제나 무통장입금으로 되어 있지 않은 경우에는 Dialog를 띄워줍니다. 모두 통과된 경우라면, ItemOrderResultPage로 이동해주면 됩니다.

Step 9: 실행한 모습입니다.



[전체 소스 코드]
[item_checkout_page.dart]
import 'package:flutter/material.dart'; | |
import 'package:cached_network_image/cached_network_image.dart'; | |
import 'package:project/components/basic_dialog.dart'; | |
import 'package:project/constants.dart'; | |
import 'package:project/item_order_result_page.dart'; | |
import 'package:project/models/product.dart'; | |
import 'package:kpostal/kpostal.dart'; | |
class ItemCheckoutPage extends StatefulWidget { | |
const ItemCheckoutPage({super.key}); | |
@override | |
State<ItemCheckoutPage> createState() => _ItemCheckoutPageState(); | |
} | |
class _ItemCheckoutPageState extends State<ItemCheckoutPage> { | |
List<Product> checkoutList = [ | |
Product( | |
productNo: 1, | |
productName: "노트북(Laptop)", | |
productImageUrl: "https://picsum.photos/id/1/300/300", | |
price: 600000), | |
Product( | |
productNo: 4, | |
productName: "키보드(Keyboard)", | |
productImageUrl: "https://picsum.photos/id/60/300/300", | |
price: 50000), | |
]; | |
List<Map<int, int>> quantityList = [ | |
{1: 2}, | |
{4: 3}, | |
]; | |
double totalPrice = 0; | |
final formKey = GlobalKey<FormState>(); | |
//! controller 변수 추가 | |
TextEditingController buyerNameController = TextEditingController(); | |
TextEditingController buyerEmailController = TextEditingController(); | |
TextEditingController buyerPhoneController = TextEditingController(); | |
TextEditingController receiverNameController = TextEditingController(); | |
TextEditingController receiverPhoneController = TextEditingController(); | |
TextEditingController receiverZipController = TextEditingController(); | |
TextEditingController receiverAddress1Controller = TextEditingController(); | |
TextEditingController receiverAddress2Controller = TextEditingController(); | |
TextEditingController userPwdController = TextEditingController(); | |
TextEditingController userConfirmPwdController = TextEditingController(); | |
TextEditingController cardNoController = TextEditingController(); | |
TextEditingController cardAuthController = TextEditingController(); | |
TextEditingController cardExpiredDateController = TextEditingController(); | |
TextEditingController cardPwdTwoDigitsController = TextEditingController(); | |
TextEditingController depositNameController = TextEditingController(); | |
//! 결제수단 옵션 선택 변수 | |
final List<String> paymentMethodList = [ | |
'결제수단선택', | |
'카드결제', | |
'무통장입금', | |
]; | |
String selectedPaymentMethod = "결제수단선택"; | |
@override | |
void initState() { | |
super.initState(); | |
for (int i = 0; i < checkoutList.length; i++) { | |
totalPrice += | |
checkoutList[i].price! * quantityList[i][checkoutList[i].productNo]!; | |
} | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
appBar: AppBar( | |
title: const Text("결제시작"), | |
centerTitle: true, | |
), | |
body: SingleChildScrollView( | |
child: Column( | |
children: [ | |
ListView.builder( | |
shrinkWrap: true, | |
itemCount: checkoutList.length, | |
itemBuilder: (context, index) { | |
return checkoutContainer( | |
productNo: checkoutList[index].productNo ?? 0, | |
productName: checkoutList[index].productName ?? "", | |
productImageUrl: checkoutList[index].productImageUrl ?? "", | |
price: checkoutList[index].price ?? 0, | |
quantity: quantityList[index] | |
[checkoutList[index].productNo] ?? | |
0); | |
}, | |
), | |
//! 입력폼 필드 | |
Form( | |
key: formKey, | |
child: Column( | |
children: [ | |
inputTextField( | |
currentController: buyerNameController, | |
currentHintText: "주문자명"), | |
inputTextField( | |
currentController: buyerEmailController, | |
currentHintText: "주문자 이메일"), | |
inputTextField( | |
currentController: buyerPhoneController, | |
currentHintText: "주문자 휴대전화"), | |
inputTextField( | |
currentController: receiverNameController, | |
currentHintText: "받는 사람 이름"), | |
inputTextField( | |
currentController: receiverPhoneController, | |
currentHintText: "받는 사람 휴대 전화"), | |
receiverZipTextField(), | |
inputTextField( | |
currentController: receiverAddress1Controller, | |
currentHintText: "기본 주소", | |
isReadOnly: true), | |
inputTextField( | |
currentController: receiverAddress2Controller, | |
currentHintText: "상세 주소"), | |
inputTextField( | |
currentController: userPwdController, | |
currentHintText: "비회원 주문조회 비밀번호", | |
isObscure: true), | |
inputTextField( | |
currentController: userConfirmPwdController, | |
currentHintText: "비회원 주문조회 비밀번호 확인", | |
isObscure: true), | |
paymentMethodDropdownButton(), | |
if (selectedPaymentMethod == "카드결제") | |
Column( | |
children: [ | |
inputTextField( | |
currentController: cardNoController, | |
currentHintText: "카드번호"), | |
inputTextField( | |
currentController: cardAuthController, | |
currentHintText: "카드명의자 주민번호 앞자리 또는 사업자번호", | |
currentMaxLength: 10), | |
inputTextField( | |
currentController: cardExpiredDateController, | |
currentHintText: "카드 만료일 (YYYYMM)", | |
currentMaxLength: 6), | |
inputTextField( | |
currentController: cardPwdTwoDigitsController, | |
currentHintText: "카드 비밀번호 앞2자리", | |
currentMaxLength: 2), | |
], | |
), | |
if (selectedPaymentMethod == "무통장입금") | |
inputTextField( | |
currentController: depositNameController, | |
currentHintText: "입금자명"), | |
], | |
), | |
), | |
], | |
), | |
), | |
bottomNavigationBar: Padding( | |
padding: const EdgeInsets.all(20), | |
child: FilledButton( | |
onPressed: () { | |
if (formKey.currentState!.validate()) { | |
if (selectedPaymentMethod == "결제수단선택") { | |
showDialog( | |
context: context, | |
barrierDismissible: true, | |
builder: (context) { | |
return BasicDialog( | |
content: "결제수단을 선택해 주세요.", | |
buttonText: "닫기", | |
buttonFunction: () => Navigator.of(context).pop(), | |
); | |
}, | |
); | |
return; | |
} | |
Navigator.of(context).push(MaterialPageRoute( | |
builder: (context) { | |
return const ItemOrderResultPage(); | |
}, | |
)); | |
} | |
}, | |
child: Text("총 ${numberFormat.format(totalPrice)}원 결제하기"), | |
)), | |
); | |
} | |
Widget checkoutContainer({ | |
required int productNo, | |
required String productName, | |
required String productImageUrl, | |
required double price, | |
required int quantity, | |
}) { | |
return Container( | |
padding: const EdgeInsets.all(8), | |
child: Row( | |
crossAxisAlignment: CrossAxisAlignment.start, | |
children: [ | |
CachedNetworkImage( | |
width: MediaQuery.of(context).size.width * 0.3, | |
fit: BoxFit.cover, | |
imageUrl: productImageUrl, | |
placeholder: (context, url) { | |
return const Center( | |
child: CircularProgressIndicator( | |
strokeWidth: 2, | |
), | |
); | |
}, | |
errorWidget: (context, url, error) { | |
return const Center( | |
child: Text("오류 발생"), | |
); | |
}, | |
), | |
Container( | |
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 8), | |
child: Column( | |
crossAxisAlignment: CrossAxisAlignment.start, | |
mainAxisSize: MainAxisSize.min, | |
children: [ | |
Text( | |
productName, | |
textScaleFactor: 1.2, | |
style: const TextStyle( | |
fontWeight: FontWeight.bold, | |
), | |
), | |
Text("${numberFormat.format(price)}원"), | |
Text("수량: $quantity"), | |
Text("합계: ${numberFormat.format(price * quantity)}원"), | |
], | |
), | |
), | |
], | |
), | |
); | |
} | |
Widget inputTextField({ | |
required TextEditingController currentController, | |
required String currentHintText, | |
int? currentMaxLength, | |
bool isObscure = false, | |
bool isReadOnly = false, | |
}) { | |
return Padding( | |
padding: const EdgeInsets.all(8.0), | |
child: TextFormField( | |
validator: (value) { | |
if (value!.isEmpty) { | |
return "내용을 입력해 주세요."; | |
} else { | |
if (currentController == userConfirmPwdController && | |
userPwdController.text != userConfirmPwdController.text) { | |
return "비밀번호가 일치하지 않습니다."; | |
} | |
} | |
return null; | |
}, | |
controller: currentController, | |
maxLength: currentMaxLength, | |
obscureText: isObscure, | |
readOnly: isReadOnly, | |
decoration: InputDecoration( | |
border: const OutlineInputBorder(), | |
hintText: currentHintText, | |
), | |
), | |
); | |
} | |
Widget receiverZipTextField() { | |
return Padding( | |
padding: const EdgeInsets.all(8.0), | |
child: Row( | |
children: [ | |
Expanded( | |
child: TextFormField( | |
readOnly: true, | |
controller: receiverZipController, | |
decoration: const InputDecoration( | |
border: OutlineInputBorder(), | |
hintText: "우편번호", | |
), | |
), | |
), | |
const SizedBox(width: 15), | |
FilledButton( | |
onPressed: () { | |
Navigator.of(context).push( | |
MaterialPageRoute( | |
builder: (context) { | |
return KpostalView(callback: (Kpostal result) { | |
receiverZipController.text = result.postCode; | |
receiverAddress1Controller.text = result.address; | |
}); | |
}, | |
), | |
); | |
}, | |
style: FilledButton.styleFrom( | |
shape: RoundedRectangleBorder( | |
borderRadius: BorderRadius.circular(5), | |
), | |
), | |
child: const Padding( | |
padding: EdgeInsets.symmetric(vertical: 22), | |
child: Text("우편 번호 찾기"), | |
), | |
), | |
], | |
), | |
); | |
} | |
Widget paymentMethodDropdownButton() { | |
return Container( | |
width: double.infinity, | |
margin: const EdgeInsets.all(8), | |
padding: const EdgeInsets.all(8), | |
decoration: BoxDecoration( | |
border: Border.all( | |
width: 0.5, | |
), | |
borderRadius: BorderRadius.circular(4), | |
), | |
child: DropdownButton<String>( | |
value: selectedPaymentMethod, | |
onChanged: (String? value) { | |
setState(() { | |
selectedPaymentMethod = value ?? ""; | |
}); | |
}, | |
isExpanded: true, | |
underline: Container(), | |
items: paymentMethodList.map<DropdownMenuItem<String>>((String value) { | |
return DropdownMenuItem<String>( | |
value: value, | |
child: Text(value), | |
); | |
}).toList(), | |
), | |
); | |
} | |
} |
[item_order_result_page.dart]
import 'package:flutter/material.dart'; | |
class BasicDialog extends StatelessWidget { | |
BasicDialog( | |
{super.key, | |
required this.content, | |
required this.buttonText, | |
required this.buttonFunction}); | |
String content; | |
String buttonText; | |
Function() buttonFunction; | |
@override | |
Widget build(BuildContext context) { | |
return AlertDialog( | |
content: Column( | |
mainAxisSize: MainAxisSize.min, | |
children: [ | |
Container( | |
padding: const EdgeInsets.only(top: 30, bottom: 15), | |
child: Center( | |
child: Text(content), | |
), | |
), | |
], | |
), | |
actions: [ | |
Center( | |
child: FilledButton( | |
onPressed: buttonFunction, | |
child: Text(buttonText), | |
), | |
), | |
], | |
); | |
} | |
} |
[유튜브 강좌 영상]
'모바일어플개발 > Flutter 실전어플 개발' 카테고리의 다른 글
[008] 플러터 (Flutter) 실전어플제작 - 쇼핑몰 앱 제작(UI구성8 - 제품 주문조회 페이지 만들기) (2) | 2023.11.18 |
---|---|
[007] 플러터 (Flutter) 실전어플제작 - 쇼핑몰 앱 제작(UI구성7 - 제품 주문완료 페이지 만들기) (0) | 2023.11.11 |
[005] 플러터 (Flutter) 실전어플제작 - 쇼핑몰 앱 제작(UI구성5 - 제품 결제시작 페이지 만들기 2) (2) | 2023.10.14 |
[004] 플러터 (Flutter) 실전어플제작 - 쇼핑몰 앱 제작(UI구성4 - 제품 결제시작 페이지 만들기 1) (1) | 2023.10.09 |
[003] 플러터 (Flutter) 실전어플제작 - 쇼핑몰 앱 제작(UI구성3 - 제품 장바구니 페이지 만들기) (0) | 2023.09.28 |