2023. 10. 9. 16:44ㆍ모바일어플개발/Flutter 실전어플 개발
안녕하세요~ totally 개발자입니다.
이번 포스팅에서는 결제시작 페이지 UI 구성에 대해 살펴보도록 하겠습니다.
Step 1: item_basket_page.dart를 복사하셔서 이름을 item_checkout_page.dart로 변경해줍니다. 클래스, State 명도 모두 변경해줍니다.

Step 2: basketList 변수를 checkoutList로 이름을 변경해주고 quantityList는 그대로 내버려둡니다.

Step 3: 아래에 있는 basketContainer 위젯의 이름을 checkoutContainer로 변경해줍니다.

Step 4: 다음 부분처럼 title과 basketList를 checkoutList로 변경해주시면 됩니다. 또한 53번째 줄에 basketContainer를 checkoutContainer로 변경해줍니다.

Step 5: item_basket_page.dart를 열어주셔서 다음처럼 onPressed 안에 결제시작 페이지로 이동할 수 있도록 Navigator push를 넣어줍니다.

Step 6: 완료되었다면 저장을 하시고 장바구니 페이지에서 총 1,350,000원 결제하기 파란색 버튼을 누르시면 아래 화면처럼 '결제시작' 페이지로 랜딩이 됩니다.

Step 7: 장바구니 페이지와 달리 결제시작에서는 수량을 수정할 수 없고 제품 또한 제거하지 못하도록 막아야 하기 때문에 여기에서는 수량만 표시해주시고 수량을 조절하는 버튼과 제거 버튼은 모두 없애줍니다. basketContainer에서 Row로 구성되어 있던 부분을 Text 위젯 하나로 모두 표현해주시면 됩니다.

Step 8: 이제 입력할 수 있는 폼을 넣어야 하는데, 여기에는 주문자, 이메일, 휴대전화, 배송지(받는 사람, 주소, 받는 사람 휴대전화, 비회원 주문조회 비밀번호), 결제수단(카드, 무통장입금 등) 정보 등 많은 입력필드가 필요합니다. 먼저 아래 TextEditingController를 쭉 추가해주시면 됩니다.

Step 9: 별도의 위젯으로 buyerNameTextField를 먼저 하나 추가해줍니다.

Step 10: body 부분을 수정합니다. ListView.builder 밑에 입력필드들을 두어야 하기 때문에 Column으로 먼저 감싼 뒤, SingleChildScrollView로 다시 감싸줍니다. 그 후에 ListView.builder 내에 shrinkWrap: true를 넣으셔야 렌더링 오류가 발생하지 않습니다. 마지막에 buyerNameTextField()를 넣어서 잘 나오는지 확인합니다.


Step 11: 나머지 부분도 반영하여 넣어줍니다 (맨 아래에 소스 코드 있습니다)


반영한 모습입니다. 이제 여기에 카드결제, 무통장입금 등 결제수단 선택과 우편번호 선택할 수 있는 패키지를 추가하는 방법을 다음 포스팅에서 진행하도록 하겠습니다.
[전체 소스 코드]
import 'package:flutter/material.dart'; | |
import 'package:cached_network_image/cached_network_image.dart'; | |
import 'package:project/constants.dart'; | |
import 'package:project/models/product.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(); | |
@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); | |
}, | |
), | |
//! 입력폼 필드 | |
buyerNameTextField(), | |
buyerEmailTextField(), | |
buyerPhoneTextField(), | |
receiverNameTextField(), | |
receiverPhoneTextField(), | |
receiverZipTextField(), | |
receiverAddress1TextField(), | |
receiverAddress2TextField(), | |
userPwdTextField(), | |
userConfirmPwdTextField(), | |
cardNoTextField(), | |
cardAuthTextField(), | |
cardExpiredDateTextField(), | |
cardPwdTwoDigitsTextField(), | |
], | |
), | |
), | |
bottomNavigationBar: Padding( | |
padding: const EdgeInsets.all(20), | |
child: FilledButton( | |
onPressed: () {}, | |
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 buyerNameTextField() { | |
return Padding( | |
padding: const EdgeInsets.all(8.0), | |
child: TextFormField( | |
controller: buyerNameController, | |
decoration: const InputDecoration( | |
border: OutlineInputBorder(), | |
hintText: "주문자명", | |
), | |
), | |
); | |
} | |
Widget buyerEmailTextField() { | |
return Padding( | |
padding: const EdgeInsets.all(8.0), | |
child: TextFormField( | |
controller: buyerEmailController, | |
decoration: const InputDecoration( | |
border: OutlineInputBorder(), | |
hintText: "주문자 이메일", | |
), | |
), | |
); | |
} | |
Widget buyerPhoneTextField() { | |
return Padding( | |
padding: const EdgeInsets.all(8.0), | |
child: TextFormField( | |
controller: buyerPhoneController, | |
decoration: const InputDecoration( | |
border: OutlineInputBorder(), | |
hintText: "주문자 휴대전화", | |
), | |
), | |
); | |
} | |
Widget receiverNameTextField() { | |
return Padding( | |
padding: const EdgeInsets.all(8.0), | |
child: TextFormField( | |
controller: receiverNameController, | |
decoration: const InputDecoration( | |
border: OutlineInputBorder(), | |
hintText: "받는 사람 이름", | |
), | |
), | |
); | |
} | |
Widget receiverPhoneTextField() { | |
return Padding( | |
padding: const EdgeInsets.all(8.0), | |
child: TextFormField( | |
controller: receiverPhoneController, | |
decoration: const InputDecoration( | |
border: OutlineInputBorder(), | |
hintText: "받는 사람 휴대 전화", | |
), | |
), | |
); | |
} | |
Widget receiverZipTextField() { | |
return Padding( | |
padding: const EdgeInsets.all(8.0), | |
child: TextFormField( | |
controller: receiverZipController, | |
decoration: const InputDecoration( | |
border: OutlineInputBorder(), | |
hintText: "우편번호", | |
), | |
), | |
); | |
} | |
Widget receiverAddress1TextField() { | |
return Padding( | |
padding: const EdgeInsets.all(8.0), | |
child: TextFormField( | |
controller: receiverAddress1Controller, | |
decoration: const InputDecoration( | |
border: OutlineInputBorder(), | |
hintText: "기본 주소", | |
), | |
), | |
); | |
} | |
Widget receiverAddress2TextField() { | |
return Padding( | |
padding: const EdgeInsets.all(8.0), | |
child: TextFormField( | |
controller: receiverAddress2Controller, | |
decoration: const InputDecoration( | |
border: OutlineInputBorder(), | |
hintText: "상세 주소", | |
), | |
), | |
); | |
} | |
Widget userPwdTextField() { | |
return Padding( | |
padding: const EdgeInsets.all(8.0), | |
child: TextFormField( | |
controller: userPwdController, | |
decoration: const InputDecoration( | |
border: OutlineInputBorder(), | |
hintText: "비회원 주문조회 비밀번호", | |
), | |
), | |
); | |
} | |
Widget userConfirmPwdTextField() { | |
return Padding( | |
padding: const EdgeInsets.all(8.0), | |
child: TextFormField( | |
controller: userConfirmPwdController, | |
decoration: const InputDecoration( | |
border: OutlineInputBorder(), | |
hintText: "비회원 주문조회 비밀번호 확인", | |
), | |
), | |
); | |
} | |
Widget cardNoTextField() { | |
return Padding( | |
padding: const EdgeInsets.all(8.0), | |
child: TextFormField( | |
controller: cardNoController, | |
decoration: const InputDecoration( | |
border: OutlineInputBorder(), | |
hintText: "카드번호", | |
), | |
), | |
); | |
} | |
Widget cardAuthTextField() { | |
return Padding( | |
padding: const EdgeInsets.all(8.0), | |
child: TextFormField( | |
controller: cardAuthController, | |
decoration: const InputDecoration( | |
border: OutlineInputBorder(), | |
hintText: "카드명의자 주민번호 앞자리", | |
), | |
), | |
); | |
} | |
Widget cardExpiredDateTextField() { | |
return Padding( | |
padding: const EdgeInsets.all(8.0), | |
child: TextFormField( | |
controller: cardExpiredDateController, | |
decoration: const InputDecoration( | |
border: OutlineInputBorder(), | |
hintText: "카드 만료일", | |
), | |
), | |
); | |
} | |
Widget cardPwdTwoDigitsTextField() { | |
return Padding( | |
padding: const EdgeInsets.all(8.0), | |
child: TextFormField( | |
controller: cardPwdTwoDigitsController, | |
decoration: const InputDecoration( | |
border: OutlineInputBorder(), | |
hintText: "카드 비밀번호 앞2자리", | |
), | |
), | |
); | |
} | |
} |
[유튜브 강좌 영상]
'모바일어플개발 > Flutter 실전어플 개발' 카테고리의 다른 글
[006] 플러터 (Flutter) 실전어플제작 - 쇼핑몰 앱 제작(UI구성6 - 제품 결제시작 페이지 만들기 3) (1) | 2023.11.04 |
---|---|
[005] 플러터 (Flutter) 실전어플제작 - 쇼핑몰 앱 제작(UI구성5 - 제품 결제시작 페이지 만들기 2) (2) | 2023.10.14 |
[003] 플러터 (Flutter) 실전어플제작 - 쇼핑몰 앱 제작(UI구성3 - 제품 장바구니 페이지 만들기) (0) | 2023.09.28 |
[002] 플러터 (Flutter) 실전어플제작 - 쇼핑몰 앱 제작(UI구성2 - 제품 상세 페이지 만들기) (0) | 2023.09.28 |
[001] 플러터 (Flutter) 실전어플제작 - 쇼핑몰 앱 제작(UI구성1 - 제품 리스트 페이지 만들기) (0) | 2023.09.24 |