[014] 플러터 (Flutter) 실전어플제작 - 쇼핑몰 앱 제작(로직구성6 - 주문 데이터 파이어스토어에 삽입하기)

2024. 2. 12. 14:50모바일어플개발/Flutter 실전어플 개발

반응형

안녕하세요~ totally 개발자입니다.

 

오늘은 결제시작 부분에서 결제 버튼을 눌렀을 때, 주문하고자 하는 제품들을 파이어스토어에 담아보도록 하겠습니다.

 

Step 1: 파이어스토어에 접속하셔서 orders 컬렉션을 생성해주시기 바랍니다. 다음을 눌러서 임의로 문서(document) 하나 추가해주시면 됩니다.

 

Step 2: lib > item_checkout_page.dart에서 checkoutContainer 부분에 혹시 height 지정을 안하신 경우에 height: 130 정도로 설정해주시기 바랍니다.

 

 

Step 3: lib > models > order.dart 파일을 열어서 아래처럼 수정해줍니다. class Order로 하면 다른 dart 파일과 겹치는 내용들이 있는 것 같아 ProductOrder로 변경하여 구분하였습니다.

 

class ProductOrder {
  String? orderNo;
  int? productNo;
  String? orderDate;
  String? buyerName;
  String? buyerEmail;
  String? buyerPhone;
  String? receiverName;
  String? receiverPhone;
  String? receiverZip;
  String? receiverAddress1;
  String? receiverAddress2;
  String? userPwd;
  String? paymentMethod;
  int? quantity;
  double? unitPrice;
  double? totalPrice;
  String? paymentStatus;
  String? deliveryStatus;

  ProductOrder({
    this.orderNo,
    this.productNo,
    this.orderDate,
    this.buyerName,
    this.buyerEmail,
    this.buyerPhone,
    this.receiverName,
    this.receiverPhone,
    this.receiverZip,
    this.receiverAddress1,
    this.receiverAddress2,
    this.userPwd,
    this.paymentMethod,
    this.quantity,
    this.unitPrice,
    this.totalPrice,
    this.paymentStatus,
    this.deliveryStatus,
  });

  ProductOrder.fromJson(Map<String, dynamic> json) {
    orderNo = json['orderNo'];
    productNo = json['productNo'];
    orderDate = json['orderDate'];
    buyerName = json['buyerName'];
    buyerEmail = json['buyerEmail'];
    buyerPhone = json['buyerPhone'];
    receiverName = json['receiverName'];
    receiverPhone = json['receiverPhone'];
    receiverZip = json['receiverZip'];
    receiverAddress1 = json['receiverAddress1'];
    receiverAddress2 = json['receiverAddress2'];
    userPwd = json['userPwd'];
    paymentMethod = json['paymentMethod'];
    quantity = json['quantity'];
    unitPrice = double.parse(json['unitPrice']);
    totalPrice = double.parse(json['totalPrice']);
    paymentStatus = json['paymentStatus'];
    deliveryStatus = json['deliveryStatus'];
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = {};
    data['orderNo'] = orderNo;
    data['productNo'] = productNo;
    data['orderDate'] = orderDate;
    data['buyerName'] = buyerName;
    data['buyerEmail'] = buyerEmail;
    data['buyerPhone'] = buyerPhone;
    data['receiverName'] = receiverName;
    data['receiverPhone'] = receiverPhone;
    data['receiverZip'] = receiverZip;
    data['receiverAddress1'] = receiverAddress1;
    data['receiverAddress2'] = receiverAddress2;
    data['userPwd'] = userPwd;
    data['paymentMethod'] = paymentMethod;
    data['quantity'] = quantity;
    data['unitPrice'] = unitPrice;
    data['totalPrice'] = totalPrice;
    data['paymentStatus'] = paymentStatus;
    data['deliveryStatus'] = deliveryStatus;
    return data;
  }
}

 

 

 

Step 4: lib > enums > delivery_status.dart 파일을 아래처럼 업데이트해줍니다.

 

enum DeliveryStatus {
  //! 상태 열거
  waiting('waiting', '배송대기'),
  delivering('delivering', '배송중'),
  delivered('delivered', '배송완료');

  //! 생성자
  const DeliveryStatus(this.status, this.statusName);

  final String status;
  final String statusName;

  //! 상태 이름 변환
  factory DeliveryStatus.getStatusName(String status) {
    return DeliveryStatus.values.firstWhere((value) => value.status == status,
        orElse: () => DeliveryStatus.waiting);
  }
}

 

여기에서 enum을 사용해서 각각의 상태를 체계적으로 관리하기 위함입니다. 보통 직접적으로 데이터를 다룰 때 위와 같이 정해진 값을 사용하는 경우에는 위처럼 enum 등을 사용하여 체계적으로 관리하는 것이 좋습니다. 

 

Step 5: lib > enums > payment_status.dart 파일도 아래처럼 수정해줍니다.

 

enum PaymentStatus {
  //! 상태 열거
  waiting('waiting', '입금대기'),
  completed('completed', '결제완료'),
  cancelled('cancelled', '주문취소');

  //! 생성자
  const PaymentStatus(this.status, this.statusName);

  final String status;
  final String statusName;

  //! 상태 이름 변환
  factory PaymentStatus.getStatusName(String status) {
    return PaymentStatus.values.firstWhere((value) => value.status == status,
        orElse: () => PaymentStatus.waiting);
  }
}

 

 

 

Step 6: 먼저 pubspec.yaml에서 crypto 패키지를 설치해주시고 item_checkout_page.dart 파일로 가셔서 intl, crypto 패키지 임포트 해주신 뒤에 아래 빨간색 박스 내용을 추가합니다. 245-247번째 줄은 비밀번호를 sha256 알고리즘으로 암호화하기 위함이고 248-249번째 줄은 주문번호를 현재 일시에 맞춰서 생성하기 위함입니다. (물론 이 경우에도 중복은 나올 수 있습니다)

 

 

 

Step 7: 빨간 박스 아래에 아래 내용을 추가해줍니다. 이 강좌에서는 한 데이터 document에 여러 제품을 포함할 수 없는 구조이며 무조건 문서 1개당 제품 하나만 담을 수 있는 구조입니다. 그래서 여러 제품을 담는 경우에는 orderNo로 묶어서 표현할 수 있습니다. 실제 업무에서는 아래처럼 하기 보다는 products 변수가 별도로 있어서 배열 형태로 들어가는 것이 권장됩니다. 다만 제 강좌에서는 최대한 간단하게 하기 위해 배제하였습니다. try catch 구문을 넣어 예외 처리 해주시고, database.collection("orders").add 메소드를 통해서 데이터를 추가하면 됩니다. 

 

//! 이 부분에 파이어스토어에 접근해서 데이터 insert 작업 진행함.
                            snapshot.data?.docs.forEach(
                              (document) {
                                ProductOrder productOrder = ProductOrder(
                                  orderNo: orderNo,
                                  productNo: document.data().productNo,
                                  orderDate: DateFormat("y-M-d h:m:s")
                                      .format(DateTime.now()),
                                  buyerName: buyerNameController.text,
                                  buyerEmail: buyerEmailController.text,
                                  buyerPhone: buyerPhoneController.text,
                                  receiverName: receiverNameController.text,
                                  receiverPhone: receiverPhoneController.text,
                                  receiverZip: receiverZipController.text,
                                  receiverAddress1:
                                      receiverAddress1Controller.text,
                                  receiverAddress2:
                                      receiverAddress2Controller.text,
                                  userPwd: hashPwd.toString(),
                                  paymentMethod: selectedPaymentMethod,
                                  quantity: cartMap[
                                      document.data().productNo.toString()],
                                  unitPrice: document.data().price,
                                  totalPrice: cartMap[document
                                          .data()
                                          .productNo
                                          .toString()] *
                                      document.data().price,
                                  paymentStatus:
                                      PaymentStatus.waiting.statusName,
                                  deliveryStatus:
                                      DeliveryStatus.waiting.statusName,
                                );
                                print(jsonEncode(productOrder));
                                try {
                                  database
                                      .collection("orders")
                                      .add(productOrder.toJson());
                                } catch (e) {
                                  showDialog(
                                    context: context,
                                    builder: (context) {
                                      return AlertDialog(
                                        content: Padding(
                                          padding: const EdgeInsets.all(15.0),
                                          child: Column(
                                            mainAxisSize: MainAxisSize.min,
                                            children: [
                                              Center(
                                                  child: Text("오류가 발생 했습니다.")),
                                            ],
                                          ),
                                        ),
                                        actions: [
                                          Center(
                                            child: FilledButton(
                                                onPressed: () =>
                                                    Navigator.pop(context),
                                                child: Text("확인")),
                                          ),
                                        ],
                                      );
                                    },
                                  );
                                  //! 아래 부분이 더 이상 호출되지 않도록 return합니다.
                                  return;
                                }
                              },
                            );

 

예외가 있는 경우 아래처럼 Dialog를 띄워서 오류가 발생했는지 띄워주시면 됩니다. (실제로는 어떤 오류인지까지 표현하는 것이 권장됩니다)

 

 

Step 8: 아래처럼 제품을 담고 입력한 뒤 테스트해봅니다.

 

제가 위 코드에 print로 productOrder 객체를 출력되도록 한 결과는 아래와 같습니다.

 

그리고 아래처럼 주문완료 페이지로 넘어가게 됩니다.

 

 

Step 9: 파이어스토어(Firestore)에서 아래처럼 바로 제품 3개의 주문이 들어간 것을 확인해볼 수 있습니다.

 

다음 시간에는 내가 주문한 데이터들을 확인해볼 수 있도록 작업을 진행하겠습니다. 감사합니다.

 

[전체 소스 코드]

 

[item_checkout_page.dart]

 

import 'dart:convert';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:intl/intl.dart';
import 'package:project/components/basic_dialog.dart';
import 'package:project/constants.dart';
import 'package:project/enums/delivery_status.dart';
import 'package:project/enums/payment_status.dart';
import 'package:project/item_order_result_page.dart';
import 'package:project/models/order.dart';
import 'package:project/models/product.dart';
import 'package:kpostal/kpostal.dart';
import 'package:crypto/crypto.dart';
class ItemCheckoutPage extends StatefulWidget {
const ItemCheckoutPage({super.key});
@override
State<ItemCheckoutPage> createState() => _ItemCheckoutPageState();
}
class _ItemCheckoutPageState extends State<ItemCheckoutPage> {
final database = FirebaseFirestore.instance;
Query<Product>? productListRef;
double totalPrice = 0;
Map<String, dynamic> cartMap = {};
Stream<QuerySnapshot<Product>>? productList;
List<int> keyList = [];
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();
//! 저장한 장바구니 리스트 가져오기
try {
cartMap =
json.decode(sharedPreferences.getString("cartMap") ?? "{}") ?? {};
} catch (e) {
debugPrint(e.toString());
cartMap = {};
}
//! 조건문에 넘길 product no 키 값 리스트를 선언 (기존 값이 string이어서 int로 변환)
cartMap.forEach(
(key, value) {
keyList.add(int.parse(key));
},
);
//! 파이어스토어에서 데이터 가져오는 Ref 변수
if (keyList.isNotEmpty) {
productListRef = FirebaseFirestore.instance
.collection("products")
.withConverter(
fromFirestore: (snapshot, _) =>
Product.fromJson(snapshot.data()!),
toFirestore: (product, _) => product.toJson())
.where("productNo", whereIn: keyList);
}
productList = productListRef?.orderBy("productNo").snapshots();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("결제시작"),
centerTitle: true,
),
body: SingleChildScrollView(
child: Column(
children: [
if (cartMap.isNotEmpty)
StreamBuilder(
stream: productList,
builder: (context, snapshot) {
if (snapshot.hasData) {
return ListView(
shrinkWrap: true,
children: snapshot.data!.docs.map((document) {
if (cartMap[document.data().productNo.toString()] !=
null) {
return checkoutContainer(
productNo: document.data().productNo ?? 0,
productName: document.data().productName ?? "",
productImageUrl:
document.data().productImageUrl ?? "",
price: document.data().price ?? 0,
quantity: cartMap[
document.data().productNo.toString()]);
}
return Container();
}).toList(),
);
} else if (snapshot.hasError) {
return const Center(
child: Text("오류가 발생 했습니다."),
);
} else {
return const Center(
child: CircularProgressIndicator(
strokeWidth: 2,
),
);
}
},
),
//! 입력폼 필드
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,
isObscure: true),
],
),
if (selectedPaymentMethod == "무통장입금")
inputTextField(
currentController: depositNameController,
currentHintText: "입금자명"),
],
),
),
],
),
),
bottomNavigationBar: cartMap.isEmpty
? const Center(
child: Text("결제할 제품이 없습니다."),
)
: StreamBuilder(
stream: productList,
builder: (context, snapshot) {
if (snapshot.hasData) {
totalPrice = 0;
snapshot.data?.docs.forEach((document) {
if (cartMap[document.data().productNo.toString()] != null) {
totalPrice +=
cartMap[document.data().productNo.toString()] *
document.data().price ??
0;
}
});
return 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;
}
List<int> bytes =
utf8.encode(userPwdController.text);
Digest hashPwd = sha256.convert(bytes);
String orderNo =
"${DateFormat("yMdhms").format(DateTime.now())}-${DateTime.now().millisecond}";
//! 이 부분에 파이어스토어에 접근해서 데이터 insert 작업 진행함.
snapshot.data?.docs.forEach(
(document) {
ProductOrder productOrder = ProductOrder(
orderNo: orderNo,
productNo: document.data().productNo,
orderDate: DateFormat("y-M-d h:m:s")
.format(DateTime.now()),
buyerName: buyerNameController.text,
buyerEmail: buyerEmailController.text,
buyerPhone: buyerPhoneController.text,
receiverName: receiverNameController.text,
receiverPhone: receiverPhoneController.text,
receiverZip: receiverZipController.text,
receiverAddress1:
receiverAddress1Controller.text,
receiverAddress2:
receiverAddress2Controller.text,
userPwd: hashPwd.toString(),
paymentMethod: selectedPaymentMethod,
quantity: cartMap[
document.data().productNo.toString()],
unitPrice: document.data().price,
totalPrice: cartMap[document
.data()
.productNo
.toString()] *
document.data().price,
paymentStatus:
PaymentStatus.waiting.statusName,
deliveryStatus:
DeliveryStatus.waiting.statusName,
);
print(jsonEncode(productOrder));
try {
database
.collection("orders")
.add(productOrder.toJson());
} catch (e) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
content: Padding(
padding: const EdgeInsets.all(15.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Center(
child: Text("오류가 발생 했습니다.")),
],
),
),
actions: [
Center(
child: FilledButton(
onPressed: () =>
Navigator.pop(context),
child: Text("확인")),
),
],
);
},
);
//! 아래 부분이 더 이상 호출되지 않도록 return합니다.
return;
}
},
);
Navigator.of(context).push(MaterialPageRoute(
builder: (context) {
return ItemOrderResultPage(
paymentMethod: selectedPaymentMethod,
paymentAmount: totalPrice,
receiverName: receiverNameController.text,
receiverPhone: receiverPhoneController.text,
zip: receiverZipController.text,
address1: receiverAddress1Controller.text,
address2: receiverAddress2Controller.text,
);
},
));
}
},
child:
Text("총 ${numberFormat.format(totalPrice)}원 결제하기"),
));
} else if (snapshot.hasError) {
return const Center(
child: Text("오류가 발생 했습니다."),
);
} else {
return const Center(
child: CircularProgressIndicator(
strokeWidth: 2,
),
);
}
},
),
);
}
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,
height: 130,
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(),
),
);
}
}

 

[유튜브 강좌 영상]

 

https://youtu.be/gGLF5fjYlec

 

 

 

반응형