2023. 12. 4. 20:18ㆍ모바일어플개발/Flutter 실전어플 개발
안녕하세요~ totally 개발자입니다.
저번 시간에 이어 이번에는 장바구니 페이지에서 각 제품의 수량을 변경하거나 원하는 제품을 삭제하는 로직을 추가해보도록 하겠습니다.
아직 데이터베이스를 구성하기 전이므로 수동 리스트로 제품 상세 내용을 불러올 것입니다.
Step 1: item_basket_page.dart 파일을 열어주셔서 basketList를 아래 productList로 넣어줍니다.
class _ItemBasketPageState extends State<ItemBasketPage> {
List<Product> productList = [
Product(
productNo: 1,
productName: "노트북(Laptop)",
productImageUrl: "https://picsum.photos/id/1/300/300",
price: 600000),
Product(
productNo: 2,
productName: "스마트폰(Phone)",
productImageUrl: "https://picsum.photos/id/20/300/300",
price: 500000),
Product(
productNo: 3,
productName: "머그컵(Cup)",
productImageUrl: "https://picsum.photos/id/30/300/300",
price: 15000),
Product(
productNo: 4,
productName: "키보드(Keyboard)",
productImageUrl: "https://picsum.photos/id/60/300/300",
price: 50000),
Product(
productNo: 5,
productName: "포도(Grape)",
productImageUrl: "https://picsum.photos/id/75/200/300",
price: 75000),
Product(
productNo: 6,
productName: "책(book)",
productImageUrl: "https://picsum.photos/id/24/200/300",
price: 24000),
];
....
}
Step 2: 50, 51번째 줄처럼 totalPrice 변수와 cartMap 변수를 선언 및 초기화합니다.

Step 3: initState에서 장바구니 리스트를 가져오고 총액을 계산해줍니다. 필자는 장바구니 리스트를 List 형태가 아닌 Map의 key: value 형태를 선택하였고 오직 제품번호와 수량만 저장하였기 때문에 별도의 데이터들을 가져오기 위해서 (여기에서는 가격), firstWhere을 활용해서 가격을 가져와서 계산하였습니다.

Step 4: body의 ListView.builder 부분을 아래처럼 만들어줍니다. 위와 같이 제품에서 필요한 상세 내용은 productList에서 가지고 옵니다.

Step 5: CachedNetworkImage 위젯의 이미지 높이를 130 정도로 설정해줍니다.

Step 6: 수량을 차감하는 부분입니다. 제품이 반드시 1개 이상이여야 하므로 1개 초과일 때만 차감시킬 수 있도록 조건문을 넣고 1개 초과 즉 2개 이상인 경우에 수량을 차감하고 디스크에 바로 반영해줍니다.

Step 7: 수량을 증가시키는 부분입니다. 위와 방식은 비슷하나 여기에서는 주문 개수 재고가 있는 경우에 if 문을 넣어 동일하게 처리할 수 있지만 필자는 생략하였습니다.

Step 8: 장바구니에서 원하는 제품을 삭제하는 로직입니다. remove 함수를 사용하여 제거할 수 있습니다.

Step 9: 총액 계산 부분을 아래처럼 수정하여 옮겨줍니다.


Step 10: -, +, 삭제 버튼을 누르면 모두 잘 작동하며 다음 강좌에서는 아래 결제하기

[전체 소스 코드]
import 'dart:convert'; | |
import 'package:cached_network_image/cached_network_image.dart'; | |
import 'package:flutter/material.dart'; | |
import 'package:project/constants.dart'; | |
import 'package:project/item_checkout_page.dart'; | |
import 'package:project/models/product.dart'; | |
class ItemBasketPage extends StatefulWidget { | |
const ItemBasketPage({super.key}); | |
@override | |
State<ItemBasketPage> createState() => _ItemBasketPageState(); | |
} | |
class _ItemBasketPageState extends State<ItemBasketPage> { | |
List<Product> productList = [ | |
Product( | |
productNo: 1, | |
productName: "노트북(Laptop)", | |
productImageUrl: "https://picsum.photos/id/1/300/300", | |
price: 600000), | |
Product( | |
productNo: 2, | |
productName: "스마트폰(Phone)", | |
productImageUrl: "https://picsum.photos/id/20/300/300", | |
price: 500000), | |
Product( | |
productNo: 3, | |
productName: "머그컵(Cup)", | |
productImageUrl: "https://picsum.photos/id/30/300/300", | |
price: 15000), | |
Product( | |
productNo: 4, | |
productName: "키보드(Keyboard)", | |
productImageUrl: "https://picsum.photos/id/60/300/300", | |
price: 50000), | |
Product( | |
productNo: 5, | |
productName: "포도(Grape)", | |
productImageUrl: "https://picsum.photos/id/75/200/300", | |
price: 75000), | |
Product( | |
productNo: 6, | |
productName: "책(book)", | |
productImageUrl: "https://picsum.photos/id/24/200/300", | |
price: 24000), | |
]; | |
double totalPrice = 0; | |
Map<String, dynamic> cartMap = {}; | |
@override | |
void initState() { | |
super.initState(); | |
//! 저장한 장바구니 리스트 가져오기 | |
cartMap = json.decode(sharedPreferences.getString("cartMap") ?? "{}") ?? {}; | |
} | |
double calculateTotalPrice() { | |
totalPrice = 0; | |
//! 총액 계산 | |
for (int i = 0; i < cartMap.length; i++) { | |
totalPrice += productList | |
.firstWhere((element) => | |
element.productNo == int.parse(cartMap.keys.elementAt(i))) | |
.price! * | |
cartMap[cartMap.keys.elementAt(i)]; | |
} | |
return totalPrice; | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
appBar: AppBar( | |
title: const Text("장바구니 페이지"), | |
centerTitle: true, | |
), | |
body: ListView.builder( | |
itemCount: cartMap.length, | |
itemBuilder: (context, index) { | |
int productNo = int.parse(cartMap.keys.elementAt(index)); | |
Product currentProduct = productList | |
.firstWhere((element) => element.productNo == productNo); | |
return basketContainer( | |
productNo: productNo, | |
productName: currentProduct.productName ?? "", | |
productImageUrl: currentProduct.productImageUrl ?? "", | |
price: currentProduct.price ?? 0, | |
quantity: cartMap[productNo.toString()]); | |
}, | |
), | |
bottomNavigationBar: Padding( | |
padding: const EdgeInsets.all(20), | |
child: FilledButton( | |
onPressed: () { | |
//! 결제시작 페이지로 이동 | |
Navigator.of(context).push(MaterialPageRoute( | |
builder: (context) { | |
return const ItemCheckoutPage(); | |
}, | |
)); | |
}, | |
child: Text("총 ${numberFormat.format(calculateTotalPrice())}원 결제하기"), | |
), | |
), | |
); | |
} | |
Widget basketContainer({ | |
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, | |
height: 130, | |
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)}원"), | |
Row( | |
children: [ | |
const Text("수량:"), | |
IconButton( | |
onPressed: () { | |
//! 수량 줄이기 (1 초과시에만 감소시킬 수 있음) | |
if (cartMap[productNo.toString()] > 1) { | |
setState(() { | |
//! 수량 1 차감 | |
cartMap[productNo.toString()]--; | |
//! 디스크에 반영 | |
sharedPreferences.setString( | |
"cartMap", json.encode(cartMap)); | |
}); | |
} | |
}, | |
icon: const Icon( | |
Icons.remove, | |
), | |
), | |
Text("$quantity"), | |
IconButton( | |
onPressed: () { | |
setState(() { | |
//! 수량 늘리기 | |
cartMap[productNo.toString()]++; | |
//! 디스크에 반영 | |
sharedPreferences.setString( | |
"cartMap", json.encode(cartMap)); | |
}); | |
}, | |
icon: const Icon( | |
Icons.add, | |
), | |
), | |
IconButton( | |
onPressed: () { | |
setState(() { | |
//! 장바구니에서 해당 제품 제거 | |
cartMap.remove(productNo.toString()); | |
//! 디스크에 반영 | |
sharedPreferences.setString( | |
"cartMap", json.encode(cartMap)); | |
}); | |
}, | |
icon: const Icon( | |
Icons.delete, | |
), | |
), | |
], | |
), | |
Text("합계: ${numberFormat.format(price * quantity)}원"), | |
], | |
), | |
), | |
], | |
), | |
); | |
} | |
} |
[유튜브 강좌 영상]