From 910c16b04e86ea67ea3d8ffb8cdb628cce82d99e Mon Sep 17 00:00:00 2001 From: cho4u4o Date: Tue, 8 Oct 2024 16:14:12 +0900 Subject: [PATCH 1/5] delete : unused import --- lib/screens/main/main_screen.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/screens/main/main_screen.dart b/lib/screens/main/main_screen.dart index 8831f6a..feb987b 100644 --- a/lib/screens/main/main_screen.dart +++ b/lib/screens/main/main_screen.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:dio/dio.dart'; import 'package:saphy/models/product.dart'; From 60ab1600efe25c4aff029911e2a8b8dd6bc4bad2 Mon Sep 17 00:00:00 2001 From: cho4u4o Date: Tue, 8 Oct 2024 21:31:06 +0900 Subject: [PATCH 2/5] fix : change api link --- lib/screens/search/search_result_screen.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/screens/search/search_result_screen.dart b/lib/screens/search/search_result_screen.dart index 3eab05d..223d8d7 100644 --- a/lib/screens/search/search_result_screen.dart +++ b/lib/screens/search/search_result_screen.dart @@ -115,8 +115,8 @@ class _SearchResultScreenState extends State { Future> searchText(String value) async { final dio = Dio(); try { - final response = await dio.get( - 'https://saphy.site/api/items?deviceType=PHONE&size=20&query=$value'); + final response = + await dio.get('https://saphy.site/api/items?size=20&query=$value'); if (response.statusCode == 200) { final data = response.data as Map; From ca72460dffdf432cb0c0bb508cc35846cc3eba2b Mon Sep 17 00:00:00 2001 From: cho4u4o Date: Sun, 13 Oct 2024 22:47:59 +0900 Subject: [PATCH 3/5] feat : api integration --- lib/main.dart | 51 +++-- lib/models/product.dart | 35 ++-- lib/screens/main/main_screen.dart | 2 +- lib/screens/products/liked_list_page.dart | 79 +++++++- lib/screens/products/product_detail_page.dart | 62 +++++- lib/screens/purchase/payment_page.dart | 188 +++++++++++++----- .../purchase/purchase_process_page.dart | 6 +- 7 files changed, 325 insertions(+), 98 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 00339df..acc36ab 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -26,31 +26,30 @@ class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { - return const ScreenController(); - // return MaterialApp( - // title: 'Saphy', - // theme: ThemeData( - // colorScheme: ColorScheme.fromSeed(seedColor: altBlack), - // useMaterial3: true, - // ), - // debugShowCheckedModeBanner: false, - // initialRoute: WelcomeScreen.id, - // routes: { - // WelcomeScreen.id: (context) => const WelcomeScreen(), - // SignupScreen.id: (context) => const SignupScreen( - // socialType: '', - // userEmail: '', - // userName: '', - // userPhotoUrl: '', - // ), - // OtpScreen.id: (context) => OtpScreen( - // verificationId: '', - // phoneNumber: '', - // onVerificationSuccess: () {}, - // ), - // SplashSellingScreen.id: (context) => const SplashSellingScreen(), - // ScreenController.id: (context) => const ScreenController(), - // }, - // ); + return MaterialApp( + title: 'Saphy', + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: altBlack), + useMaterial3: true, + ), + debugShowCheckedModeBanner: false, + initialRoute: WelcomeScreen.id, + routes: { + WelcomeScreen.id: (context) => const WelcomeScreen(), + SignupScreen.id: (context) => const SignupScreen( + socialType: '', + userEmail: '', + userName: '', + userPhotoUrl: '', + ), + OtpScreen.id: (context) => OtpScreen( + verificationId: '', + phoneNumber: '', + onVerificationSuccess: () {}, + ), + SplashSellingScreen.id: (context) => const SplashSellingScreen(), + ScreenController.id: (context) => const ScreenController(), + }, + ); } } diff --git a/lib/models/product.dart b/lib/models/product.dart index 6068d2e..f743739 100644 --- a/lib/models/product.dart +++ b/lib/models/product.dart @@ -10,6 +10,7 @@ class Product { String color; String storage; String grade; + ImageModel descriptionImage; Product({ required this.id, @@ -23,24 +24,27 @@ class Product { required this.color, required this.storage, required this.grade, + required this.descriptionImage, }); factory Product.fromJson(Map json) { return Product( - id: json['id'], - deviceType: json['deviceType'] ?? "", - name: json['name'] ?? "", - description: json['description'] ?? "", - price: json['price'], - stock: json['stock'] ?? "", - images: (json['images'] as List) - .map((item) => ImageModel.fromJson(item)) - .toList(), - brand: json['brand'] ?? "", - color: json['color'] ?? "", - storage: json['storage'] ?? "", - grade: json['grade'] ?? "", - ); + id: json['id'], + deviceType: json['deviceType'] ?? "", + name: json['name'] ?? "", + description: json['description'] ?? "", + price: json['price'], + stock: json['stock'] ?? "", + images: (json['images'] as List) + .map((item) => ImageModel.fromJson(item)) + .toList(), + brand: json['brand'] ?? "", + color: json['color'] ?? "", + storage: json['storage'] ?? "", + grade: json['grade'] ?? "", + descriptionImage: json['descriptionImage'] != null + ? ImageModel.fromJson(json['descriptionImage']) + : ImageModel(name: "", url: "")); } Map toJson() { @@ -51,11 +55,12 @@ class Product { 'description': description, 'price': price, 'stock': stock, - 'images': images, // 직접 저장 + 'images': images, 'brand': brand, 'color': color, 'storage': storage, 'grade': grade, + "descriptionImage": images, }; } } diff --git a/lib/screens/main/main_screen.dart b/lib/screens/main/main_screen.dart index feb987b..b0b7a63 100644 --- a/lib/screens/main/main_screen.dart +++ b/lib/screens/main/main_screen.dart @@ -145,7 +145,7 @@ class _MainScreenState extends State { } else if (!snapshot.hasData || snapshot.data!.isEmpty) { return const Center( - child: Text('No products found')); // 데이터 없음 메시지 + child: Text('상품이 없습니다')); // 데이터 없음 메시지 } else { final products = snapshot.data!; // 데이터 가져오기 return Wrap( diff --git a/lib/screens/products/liked_list_page.dart b/lib/screens/products/liked_list_page.dart index ba9292a..d2b01ee 100644 --- a/lib/screens/products/liked_list_page.dart +++ b/lib/screens/products/liked_list_page.dart @@ -3,10 +3,12 @@ import 'package:flutter/widgets.dart'; // import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:intl/intl.dart'; import 'package:saphy/models/product.dart'; +import 'package:saphy/service/authentication/secure_storage.dart'; import 'package:saphy/utils/textstyles.dart'; import 'package:saphy/widgets/product_card.dart'; import 'package:saphy/utils/colors.dart'; import 'package:saphy/widgets/app_bar.dart'; +import 'package:dio/dio.dart'; class LikedListPage extends StatefulWidget { const LikedListPage({super.key}); @@ -17,6 +19,53 @@ class LikedListPage extends StatefulWidget { class _LikedListPageState extends State { final NumberFormat numberFormat = NumberFormat('###,###,###,###'); + late Future> _products; + int cnt = 0; + + Future> getProducts() async { + final dio = Dio(); + String? accessToken = await readAccessToken(); + + try { + final response = await dio.get( + 'https://saphy.site/item-wishes/', + options: Options( + headers: { + 'Authorization': 'Bearer $accessToken', // 필요한 헤더 추가 + }, + ), + ); + if (response.statusCode == 200) { + final data = response.data as Map; + if (data['results'] != null) { + List products = (data['results'] as List) + .map((item) => Product.fromJson(item)) + .toList(); + return products; + } else { + throw Exception('No results found in the response'); + } + } else { + throw Exception('Failed to load products'); + } + } catch (e) { + return []; + } + } + + @override + void initState() { + super.initState(); + _products = getProducts(); + countProducts(); + } + + Future countProducts() async { + List products = await getProducts(); + setState(() { + cnt = products.length; + }); + } @override Widget build(BuildContext context) { @@ -53,15 +102,39 @@ class _LikedListPageState extends State { ), ), ), - const SliverPadding( - padding: EdgeInsets.all(20), + SliverPadding( + padding: const EdgeInsets.all(20), sliver: SliverToBoxAdapter( child: Wrap( direction: Axis.horizontal, alignment: WrapAlignment.spaceBetween, spacing: 15, runSpacing: 15, - children: [], + children: [ + FutureBuilder( + future: _products, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } else if (snapshot.hasError) { + return Text('Error: ${snapshot.error.toString()}'); + } else if (!snapshot.hasData || snapshot.data!.isEmpty) { + return const Center(child: Text('상품이 없습니다')); + } else { + final products = snapshot.data!; + return Wrap( + direction: Axis.horizontal, + alignment: WrapAlignment.spaceBetween, + spacing: 15, + runSpacing: 15, + children: products + .map((product) => ProductCard(product: product)) + .toList(), + ); + } + }, + ) + ], ), ), ), diff --git a/lib/screens/products/product_detail_page.dart b/lib/screens/products/product_detail_page.dart index 392aa45..95b1463 100644 --- a/lib/screens/products/product_detail_page.dart +++ b/lib/screens/products/product_detail_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:saphy/screens/purchase/purchase_page.dart'; +import 'package:saphy/service/authentication/secure_storage.dart'; import 'package:saphy/utils/colors.dart'; import 'package:saphy/utils/number_format.dart'; import 'package:saphy/utils/textstyles.dart'; @@ -17,6 +18,7 @@ class ProductDetail extends StatefulWidget { class _ProductDetailState extends State { Product? productDetail; + bool isWished = false; // 아이템이 찜되었는지 상태를 관리 @override void initState() { @@ -62,10 +64,58 @@ class _ProductDetailState extends State { color: "color", storage: "storage", grade: "grade", + descriptionImage: ImageModel(name: "name", url: "url"), ); } } + Future toggleWish() async { + final dio = Dio(); + try { + String token = await readJwt(); + token = token.toString().split(" ")[2]; + + if (!isWished) { + // 아이템을 찜하는 POST 요청 + final response = await dio.post( + 'https://saphy.site/item-wishes?itemId=${widget.product.id}', + options: Options( + headers: { + 'Authorization': token, + }, + ), + ); + + if (response.statusCode == 201) { + // 요청이 성공적으로 처리된 경우 + setState(() { + isWished = true; // 아이템이 찜 상태로 변경 + }); + } + } else { + // 아이템 찜 해제하는 POST 요청 + final response = await dio.delete( + 'https://saphy.site/item-wishes/${widget.product.id}', // 삭제 요청을 보낼 URL + options: Options( + headers: { + 'Authorization': 'Bearer $token', + }, + ), + ); + + if (response.statusCode == 200) { + // 요청이 성공적으로 처리된 경우 + setState(() { + isWished = false; // 아이템이 찜 해제 상태로 변경 + }); + } + } + } catch (e) { + print('Error: $e'); + // 에러 처리 로직 추가 가능 + } + } + @override Widget build(BuildContext context) { var screenWidth = MediaQuery.of(context).size.width; @@ -350,8 +400,16 @@ class _ProductDetailState extends State { crossAxisAlignment: CrossAxisAlignment.center, children: [ IconButton( - onPressed: () {}, - icon: const Icon(Icons.favorite_outline)), + onPressed: toggleWish, + icon: Icon( + isWished + ? Icons.favorite + : Icons.favorite_outline, // 찜 상태에 따라 아이콘 변경 + color: isWished + ? Colors.red + : Colors.black, // 찜 상태에 따라 색상 변경 + ), + ), const SizedBox(width: 10), Flexible(flex: 1, child: button("구매하기")), const SizedBox(width: 10), diff --git a/lib/screens/purchase/payment_page.dart b/lib/screens/purchase/payment_page.dart index c63bbc7..8934c3a 100644 --- a/lib/screens/purchase/payment_page.dart +++ b/lib/screens/purchase/payment_page.dart @@ -1,9 +1,11 @@ -import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:iamport_flutter/iamport_payment.dart'; import 'package:iamport_flutter/model/payment_data.dart'; import 'package:saphy/models/product.dart'; import 'package:saphy/screens/purchase/purchase_success.dart'; +import 'package:saphy/service/api_service.dart'; +import 'package:saphy/service/authentication/secure_storage.dart'; +import 'package:saphy/utils/colors.dart'; class Payment extends StatelessWidget { final Product product; @@ -11,74 +13,162 @@ class Payment extends StatelessWidget { @override Widget build(BuildContext context) { - return IamportPayment( - initialChild: Container( - color: Colors.white, - child: const Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text('잠시만 기다려주세요...', style: TextStyle(fontSize: 20)), - ], - ), - ), - ), - userCode: 'imp16147707', - data: PaymentData( - pg: 'tosspayments', - payMethod: 'card', - name: product.name, - merchantUid: 'mid_${DateTime.now().millisecondsSinceEpoch}', - amount: product.price, - buyerName: '홍길동', - buyerTel: '01012345678', - buyerEmail: 'example@naver.com', - buyerAddr: '서울시 강남구 신사동 661-16', - buyerPostcode: '06018', - appScheme: 'example', - cardQuota: [2, 3]), - callback: (Map result) async { - if (result['success'] == 'true') { - String? impUid = result['imp_uid']; - String? merchantUid = result['merchant_uid']; - await verifyIamport(impUid, merchantUid, context); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const PurchaseSuccess(), + return FutureBuilder( + future: preparePayment(product), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center( + child: CircularProgressIndicator( + backgroundColor: white, ), ); - } else { - String? errorMsg = result['error_msg']; - print('결제 실패: $errorMsg'); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('결제 실패: $errorMsg')), + } + + if (snapshot.hasError) { + return const Center( + child: Text('결제 준비 실패'), // 결제 준비가 실패한 경우 표시할 UI + ); + } + + if (snapshot.hasData) { + final paymentData = snapshot.data as Map; + final String merchantUid = paymentData['merchantUid']; + final double amount = paymentData['amount']; + + return IamportPayment( + initialChild: Container( + color: Colors.white, + child: const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('잠시만 기다려주세요...', style: TextStyle(fontSize: 20)), + ], + ), + ), + ), + userCode: 'imp16147707', + data: PaymentData( + pg: 'tosspayments', + payMethod: 'card', + name: product.name, + merchantUid: merchantUid, // 준비된 merchantUid 사용 + amount: amount, // 준비된 amount 사용 + buyerName: '홍길동', + buyerTel: '01012345678', + buyerEmail: 'example@naver.com', + buyerAddr: '서울시 강남구 신사동 661-16', + buyerPostcode: '06018', + appScheme: 'example', + cardQuota: [2, 3], + ), + callback: (Map result) async { + if (result['success'] == 'true') { + String? impUid = result['imp_uid']; + String? merchantUid = result['merchant_uid']; + await verifyIamport(impUid, merchantUid, context); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const PurchaseSuccess(), + ), + ); + } else { + String? errorMsg = result['error_msg']; + print('결제 실패: $errorMsg'); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('결제 실패: $errorMsg')), + ); + } + }, ); } + + // 기본적으로 로딩 UI를 표시 + return const Center( + child: CircularProgressIndicator(), // 기본적으로 결제 준비 중 로딩 표시 + ); }, ); } + // preparePayment 함수는 결제 준비 데이터를 서버에서 받아오는 역할 + Future?> preparePayment(Product product) async { + String token = await readJwt(); + token = token.toString().split(" ")[2]; + + final param = { + 'itemId': product.id, + 'quantity': 1, + 'amount': product.price, + 'payMethod': 'CREDIT_CARD', + }; + + try { + final response = await APIService.instance.request( + '/payments/prepare', + DioMethod.post, + contentType: 'application/json', + token: "Bearer $token", + param: param, + ); + + if (response.statusCode == 200 && response.data != null) { + final data = response.data; + final merchantUid = data['results'][0]['merchantUid']; + final amount = data['results'][0]['amount']; + return {'merchantUid': merchantUid, 'amount': amount}; + } else { + throw Exception('결제 준비 실패'); + } + } catch (e) { + print('Error: $e'); + return null; + } + } + + // 결제 후 Iamport에서 검증하는 함수 Future verifyIamport( String? impUid, String? itemId, BuildContext context) async { - final dio = Dio(); + // JWT 토큰 읽기 + String token = await readJwt(); + token = token.toString().split(" ")[2]; + // impUid 또는 itemId가 없을 경우 중단 if (impUid == null || itemId == null) return; - final data = { - 'itemId': itemId, - 'impUid': impUid, + // 결제 검증에 사용할 파라미터 + final param = { + "merchantUid": + "ORD-8d323fff-339a-4707-89b1-7b1cbb1681f6", // 실제 merchantUid 값을 가져오거나 사용 + "itemId": itemId, + "impUid": impUid, // Iamport에서 반환된 impUid를 사용 + "amount": 999.99 // 실제 결제 금액을 사용 }; try { - final response = await dio.post( - 'https://saphy.site/payments', - data: data, // 수정된 부분 + // API 요청 보내기 (POST) + final response = await APIService.instance.request( + '/payments/complete', + DioMethod.post, + contentType: 'application/json', + token: "Bearer $token", // JWT 토큰을 헤더에 포함 + param: param, ); if (response.statusCode == 200) { - } else {} + // 결제 검증 성공 처리 + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('결제 검증 성공')), + ); + } else { + // 실패 처리 + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('결제 검증 실패')), + ); + } } catch (e) { + // 예외 처리 print('Error: $e'); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('서버와 연결할 수 없습니다.')), diff --git a/lib/screens/purchase/purchase_process_page.dart b/lib/screens/purchase/purchase_process_page.dart index 83a0030..20000b7 100644 --- a/lib/screens/purchase/purchase_process_page.dart +++ b/lib/screens/purchase/purchase_process_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:saphy/screens/purchase/payment_page.dart'; import 'package:saphy/utils/colors.dart'; +import 'package:saphy/utils/number_format.dart'; import 'package:saphy/utils/textstyles.dart'; import 'package:saphy/widgets/normal_button.dart'; import 'package:saphy/models/product.dart'; @@ -118,10 +119,11 @@ class _PurchaseProcessPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(widget.product.name, style: subTitleText()), - Text("${widget.product.storage}, ${widget.product.color}", + Text("${widget.product.storage} ${widget.product.color}", style: bodyText()), const Spacer(), - Text("${widget.product.price}", style: subTitleText()), + Text(numberFormat.format(widget.product.price), + style: subTitleText()), ], ), ), From 9c552d04baa08a34f8845710dc176f3ad3d375dd Mon Sep 17 00:00:00 2001 From: cho4u4o Date: Mon, 14 Oct 2024 03:16:11 +0900 Subject: [PATCH 4/5] fix : fix payment logic --- lib/screens/purchase/payment_page.dart | 182 ++++++++++++------------ lib/screens/purchase/purchase_fail.dart | 26 ++-- 2 files changed, 109 insertions(+), 99 deletions(-) diff --git a/lib/screens/purchase/payment_page.dart b/lib/screens/purchase/payment_page.dart index 8934c3a..21f7594 100644 --- a/lib/screens/purchase/payment_page.dart +++ b/lib/screens/purchase/payment_page.dart @@ -2,10 +2,12 @@ import 'package:flutter/material.dart'; import 'package:iamport_flutter/iamport_payment.dart'; import 'package:iamport_flutter/model/payment_data.dart'; import 'package:saphy/models/product.dart'; +import 'package:saphy/screens/purchase/purchase_fail.dart'; import 'package:saphy/screens/purchase/purchase_success.dart'; import 'package:saphy/service/api_service.dart'; import 'package:saphy/service/authentication/secure_storage.dart'; import 'package:saphy/utils/colors.dart'; +import 'package:saphy/widgets/normal_button.dart'; class Payment extends StatelessWidget { final Product product; @@ -13,86 +15,93 @@ class Payment extends StatelessWidget { @override Widget build(BuildContext context) { - return FutureBuilder( - future: preparePayment(product), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center( - child: CircularProgressIndicator( - backgroundColor: white, - ), - ); - } - - if (snapshot.hasError) { - return const Center( - child: Text('결제 준비 실패'), // 결제 준비가 실패한 경우 표시할 UI - ); - } + return Scaffold( + backgroundColor: altWhite, + body: FutureBuilder( + future: preparePayment(product), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center( + child: CircularProgressIndicator( + backgroundColor: white, + ), + ); + } - if (snapshot.hasData) { - final paymentData = snapshot.data as Map; - final String merchantUid = paymentData['merchantUid']; - final double amount = paymentData['amount']; + if (snapshot.data != null) { + final paymentData = snapshot.data as Map; + final String merchantUid = paymentData['merchantUid']; + final double amount = paymentData['amount']; - return IamportPayment( - initialChild: Container( - color: Colors.white, - child: const Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text('잠시만 기다려주세요...', style: TextStyle(fontSize: 20)), - ], + return IamportPayment( + initialChild: Container( + color: Colors.white, + child: const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('잠시만 기다려주세요...', style: TextStyle(fontSize: 20)), + ], + ), ), ), - ), - userCode: 'imp16147707', - data: PaymentData( - pg: 'tosspayments', - payMethod: 'card', - name: product.name, - merchantUid: merchantUid, // 준비된 merchantUid 사용 - amount: amount, // 준비된 amount 사용 - buyerName: '홍길동', - buyerTel: '01012345678', - buyerEmail: 'example@naver.com', - buyerAddr: '서울시 강남구 신사동 661-16', - buyerPostcode: '06018', - appScheme: 'example', - cardQuota: [2, 3], - ), - callback: (Map result) async { - if (result['success'] == 'true') { - String? impUid = result['imp_uid']; - String? merchantUid = result['merchant_uid']; - await verifyIamport(impUid, merchantUid, context); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const PurchaseSuccess(), - ), - ); - } else { - String? errorMsg = result['error_msg']; - print('결제 실패: $errorMsg'); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('결제 실패: $errorMsg')), - ); - } - }, - ); - } + userCode: 'imp16147707', + data: PaymentData( + pg: 'tosspayments', + payMethod: 'card', + name: product.name, + merchantUid: merchantUid, + amount: amount, + buyerName: '홍길동', + buyerTel: '01012345678', + buyerEmail: 'example@naver.com', + buyerAddr: '서울시 강남구 신사동 661-16', + buyerPostcode: '06018', + appScheme: 'example', + cardQuota: [2, 3], + ), + callback: (Map result) async { + print('결제 결과: $result'); - // 기본적으로 로딩 UI를 표시 - return const Center( - child: CircularProgressIndicator(), // 기본적으로 결제 준비 중 로딩 표시 - ); - }, + if (result['success'] == 'true') { + String? impUid = result['imp_uid']; + String? merchantUid = result['merchant_uid']; + await verifyIamport(product.id, impUid, merchantUid, context); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const PurchaseSuccess(), + ), + ); + } else { + String? errorMsg = result['error_msg']; + print('결제 실패: $errorMsg'); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('결제 실패: $errorMsg')), + ); + } + }, + ); + } + return const Text("결제 실패!"); + // return Center( + // child: Column( + // children: [ + // const Text("결제 실패"), + // NormalButton( + // title: "홈으로 돌아가기", + // bgColor: black, + // txtColor: white, + // onTap: () {}, + // flag: true) + // ], + // ), + // ); + }, + ), ); } - // preparePayment 함수는 결제 준비 데이터를 서버에서 받아오는 역할 Future?> preparePayment(Product product) async { String token = await readJwt(); token = token.toString().split(" ")[2]; @@ -100,20 +109,22 @@ class Payment extends StatelessWidget { final param = { 'itemId': product.id, 'quantity': 1, - 'amount': product.price, + 'amount': '${product.price}.00', 'payMethod': 'CREDIT_CARD', }; try { final response = await APIService.instance.request( - '/payments/prepare', + 'https://saphy.site/payments/prepare', DioMethod.post, contentType: 'application/json', token: "Bearer $token", param: param, ); - if (response.statusCode == 200 && response.data != null) { + final statusCode = response.data['status']['code']; + + if (statusCode == 200) { final data = response.data; final merchantUid = data['results'][0]['merchantUid']; final amount = data['results'][0]['amount']; @@ -128,31 +139,26 @@ class Payment extends StatelessWidget { } // 결제 후 Iamport에서 검증하는 함수 - Future verifyIamport( - String? impUid, String? itemId, BuildContext context) async { - // JWT 토큰 읽기 + Future verifyIamport(int itemId, String? impUid, String? merchantUid, + BuildContext context) async { String token = await readJwt(); token = token.toString().split(" ")[2]; - // impUid 또는 itemId가 없을 경우 중단 - if (impUid == null || itemId == null) return; + if (impUid == null) return; - // 결제 검증에 사용할 파라미터 final param = { - "merchantUid": - "ORD-8d323fff-339a-4707-89b1-7b1cbb1681f6", // 실제 merchantUid 값을 가져오거나 사용 + "merchantUid": merchantUid, "itemId": itemId, - "impUid": impUid, // Iamport에서 반환된 impUid를 사용 - "amount": 999.99 // 실제 결제 금액을 사용 + "impUid": impUid, + "amount": "${product.price}.00" }; try { - // API 요청 보내기 (POST) final response = await APIService.instance.request( - '/payments/complete', + 'https://saphy.site/payments/complete', DioMethod.post, contentType: 'application/json', - token: "Bearer $token", // JWT 토큰을 헤더에 포함 + token: "Bearer $token", param: param, ); diff --git a/lib/screens/purchase/purchase_fail.dart b/lib/screens/purchase/purchase_fail.dart index 2f3675d..3da4a9d 100644 --- a/lib/screens/purchase/purchase_fail.dart +++ b/lib/screens/purchase/purchase_fail.dart @@ -7,17 +7,21 @@ class PurchaseFail extends StatelessWidget { @override Widget build(BuildContext context) { - return Center( - child: Column( - children: [ - const Text("결제 실패"), - NormalButton( - title: "홈으로 돌아가기", - bgColor: black, - txtColor: white, - onTap: () {}, - flag: true) - ], + final screenHeight = MediaQuery.of(context).size.height; + return SizedBox( + height: screenHeight, + child: Center( + child: Column( + children: [ + const Text("결제 실패"), + NormalButton( + title: "홈으로 돌아가기", + bgColor: black, + txtColor: white, + onTap: () {}, + flag: true) + ], + ), ), ); } From 39e337b01f109bff46334c41843c7f84518d4a36 Mon Sep 17 00:00:00 2001 From: cho4u4o Date: Tue, 15 Oct 2024 12:41:41 +0900 Subject: [PATCH 5/5] feat : complete purchase process --- lib/screens/purchase/payment_page.dart | 16 ++++++++++++++-- lib/screens/purchase/purchase_fail.dart | 11 ++++++++++- lib/screens/purchase/purchase_success.dart | 11 ++++++++++- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/lib/screens/purchase/payment_page.dart b/lib/screens/purchase/payment_page.dart index 21f7594..6125bfc 100644 --- a/lib/screens/purchase/payment_page.dart +++ b/lib/screens/purchase/payment_page.dart @@ -63,11 +63,11 @@ class Payment extends StatelessWidget { callback: (Map result) async { print('결제 결과: $result'); - if (result['success'] == 'true') { + if (result['error_msg'] == null) { String? impUid = result['imp_uid']; String? merchantUid = result['merchant_uid']; await verifyIamport(product.id, impUid, merchantUid, context); - Navigator.push( + Navigator.pushReplacement( context, MaterialPageRoute( builder: (context) => const PurchaseSuccess(), @@ -79,6 +79,12 @@ class Payment extends StatelessWidget { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('결제 실패: $errorMsg')), ); + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => const PurchaseFail(), + ), + ); } }, ); @@ -167,6 +173,12 @@ class Payment extends StatelessWidget { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('결제 검증 성공')), ); + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => const PurchaseSuccess(), + ), + ); } else { // 실패 처리 ScaffoldMessenger.of(context).showSnackBar( diff --git a/lib/screens/purchase/purchase_fail.dart b/lib/screens/purchase/purchase_fail.dart index 3da4a9d..a7cca28 100644 --- a/lib/screens/purchase/purchase_fail.dart +++ b/lib/screens/purchase/purchase_fail.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:saphy/screens/main/main_screen.dart'; import 'package:saphy/utils/colors.dart'; import 'package:saphy/widgets/normal_button.dart'; @@ -18,7 +19,15 @@ class PurchaseFail extends StatelessWidget { title: "홈으로 돌아가기", bgColor: black, txtColor: white, - onTap: () {}, + onTap: () { + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute( + builder: (context) => const MainScreen(), + ), + (route) => false, + ); + }, flag: true) ], ), diff --git a/lib/screens/purchase/purchase_success.dart b/lib/screens/purchase/purchase_success.dart index bdbad1a..45d2deb 100644 --- a/lib/screens/purchase/purchase_success.dart +++ b/lib/screens/purchase/purchase_success.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:saphy/screens/main/main_screen.dart'; import 'package:saphy/utils/colors.dart'; import 'package:saphy/widgets/normal_button.dart'; @@ -31,7 +32,15 @@ class PurchaseSuccess extends StatelessWidget { title: "홈으로 돌아가기", bgColor: black, txtColor: white, - onTap: () {}, + onTap: () { + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute( + builder: (context) => const MainScreen(), + ), + (route) => false, + ); + }, flag: true) ], ),