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 8831f6a..b0b7a63 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'; @@ -146,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..6125bfc 100644 --- a/lib/screens/purchase/payment_page.dart +++ b/lib/screens/purchase/payment_page.dart @@ -1,9 +1,13 @@ -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_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; @@ -11,74 +15,178 @@ 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)), - ], - ), - ), + 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.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)), + ], + ), + ), + ), + 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'); + + if (result['error_msg'] == null) { + String? impUid = result['imp_uid']; + String? merchantUid = result['merchant_uid']; + await verifyIamport(product.id, impUid, merchantUid, context); + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => const PurchaseSuccess(), + ), + ); + } else { + String? errorMsg = result['error_msg']; + print('결제 실패: $errorMsg'); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('결제 실패: $errorMsg')), + ); + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => const PurchaseFail(), + ), + ); + } + }, + ); + } + return const Text("결제 실패!"); + // return Center( + // child: Column( + // children: [ + // const Text("결제 실패"), + // NormalButton( + // title: "홈으로 돌아가기", + // bgColor: black, + // txtColor: white, + // onTap: () {}, + // flag: true) + // ], + // ), + // ); + }, ), - 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(), - ), - ); - } else { - String? errorMsg = result['error_msg']; - print('결제 실패: $errorMsg'); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('결제 실패: $errorMsg')), - ); - } - }, ); } - Future verifyIamport( - String? impUid, String? itemId, BuildContext context) async { - final dio = Dio(); + Future?> preparePayment(Product product) async { + String token = await readJwt(); + token = token.toString().split(" ")[2]; + + final param = { + 'itemId': product.id, + 'quantity': 1, + 'amount': '${product.price}.00', + 'payMethod': 'CREDIT_CARD', + }; + + try { + final response = await APIService.instance.request( + 'https://saphy.site/payments/prepare', + DioMethod.post, + contentType: 'application/json', + token: "Bearer $token", + param: param, + ); - if (impUid == null || itemId == null) return; + final statusCode = response.data['status']['code']; - final data = { - 'itemId': itemId, - 'impUid': impUid, + if (statusCode == 200) { + 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(int itemId, String? impUid, String? merchantUid, + BuildContext context) async { + String token = await readJwt(); + token = token.toString().split(" ")[2]; + + if (impUid == null) return; + + final param = { + "merchantUid": merchantUid, + "itemId": itemId, + "impUid": impUid, + "amount": "${product.price}.00" }; try { - final response = await dio.post( - 'https://saphy.site/payments', - data: data, // 수정된 부분 + final response = await APIService.instance.request( + 'https://saphy.site/payments/complete', + DioMethod.post, + contentType: 'application/json', + token: "Bearer $token", + param: param, ); if (response.statusCode == 200) { - } else {} + // 결제 검증 성공 처리 + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('결제 검증 성공')), + ); + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => const PurchaseSuccess(), + ), + ); + } 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_fail.dart b/lib/screens/purchase/purchase_fail.dart index 2f3675d..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'; @@ -7,17 +8,29 @@ 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: () { + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute( + builder: (context) => const MainScreen(), + ), + (route) => false, + ); + }, + flag: true) + ], + ), ), ); } 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()), ], ), ), 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) ], ), 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;