모바일어플개발/Flutter 실전어플 개발

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

Totally 개발자 2023. 10. 14. 21:40
반응형

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

 

저번 포스팅에 이어 결제시작 페이지를 작업해보겠습니다. 먼저 우편번호 부분을 API를 통해 구현할 것입니다.

 

Step 1: pubspec.yaml에 kpostal 패키지를 추가하고 터미널에 flutter pub get 입력해줍니다.

 

 

kpostal 패키지 참고

https://pub.dev/packages/kpostal

 

kpostal | Flutter Package

Kpostal package can search for Korean postal addresses using Kakao postcode service. This package is inspired by Kopo package that is discontinued.

pub.dev

 

Step 2: 안드로이드와 ios 세팅을 위해 아래 부분을 넣어줍니다.

 

안드로이드 (android > app > src > main > AndroidManifest.xml

IOS (ios > Runner > Info.plist)

 

 

Step 3: item_checkout_page.dart에 import 해줍니다.

 

 

 

Step 4: 우편번호를 위해 receiverZipTextField 부분을 수정해줍니다. 여기에는 Row로 감싸주고 TextFormField 바깥에 Expanded 위젯을 감싸주어 constraints 오류가 없도록 작업해줍니다. 빨간색 박스 부분 특히 KpostalView 안에 callback 부분은 우편번호를 검색하고 선택하고 데이터를 불러오기 위함입니다. 

 

그 후 위처럼 readOnly: true를 넣어 수정이 불가능하게 만들어줍니다.

 

결과는 아래와 같습니다.

 

 

위와 같이 우편번호 작업을 간단하게 마무리할 수 있습니다.

 

 

Step 5: 다음으로 비회원 주문조회 비밀번호 부분에 텍스트가 보이지 않도록 설정하여 줍니다. (아직 validate는 진행하지 않습니다)

 

 

Step 6: 카드 정보 입력 부분도 다음처럼 지정합니다 maxLength는 최대 문자 길이이고 카드명의자 부분에는 주민번호 앞자리 6자리가 들어갈 수도 있고 법인카드처럼 상황에 따라 사업자등록번호가 들어가는 경우도 있어 최대 10자리로 지정하였습니다. 카드만료일은 연도+월이므로(예 202705) 6자리로 지정하였고 카드 비밀번호 앞2자리 역시 가려야 하므로 obscureText: true를 지정하면 됩니다. 

 

 

이 포스팅에서는 여기까지 진행하고 다음 시간에는 결제수단 선택 등 결제시작 페이지 UI 마무리 작업을 하도록 하겠습니다. 감사합니다.

 

[전체 소스 코드]

 

import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:project/constants.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;
//! 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(
itemCount: checkoutList.length,
shrinkWrap: true,
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(
imageUrl: productImageUrl,
width: MediaQuery.of(context).size.width * 0.3,
fit: BoxFit.cover,
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: 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.0),
child: Text("우편 번호 찾기"),
),
),
],
),
);
}
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: "비회원 주문조회 비밀번호",
),
obscureText: true,
),
);
}
Widget userConfirmPwdTextField() {
return Padding(
padding: const EdgeInsets.all(8.0),
child: TextFormField(
controller: userConfirmPwdController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
hintText: "비회원 주문조회 비밀번호 확인",
),
obscureText: true,
),
);
}
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,
maxLength: 10,
decoration: const InputDecoration(
border: OutlineInputBorder(),
hintText: "카드명의자 주민번호 앞자리",
),
),
);
}
Widget cardExpiredDateTextField() {
return Padding(
padding: const EdgeInsets.all(8.0),
child: TextFormField(
controller: cardExpiredDateController,
maxLength: 6,
decoration: const InputDecoration(
border: OutlineInputBorder(),
hintText: "카드 만료일 (YYYYMM)",
),
),
);
}
Widget cardPwdTwoDigitsTextField() {
return Padding(
padding: const EdgeInsets.all(8.0),
child: TextFormField(
maxLength: 2,
controller: cardPwdTwoDigitsController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
hintText: "카드 비밀번호 앞2자리",
),
obscureText: true,
),
);
}
}

 

반응형