[006] 플러터 (Flutter) 실전어플제작 - 쇼핑몰 앱 제작(UI구성6 - 제품 결제시작 페이지 만들기 3)

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),
),
),
],
);
}
}

 

[유튜브 강좌 영상]

 

https://youtu.be/luWTQ1tKIHc

 

반응형