From ab535a71c1b17a3759726cbaf06b0c072f5e5c84 Mon Sep 17 00:00:00 2001 From: ottuck <116790133+ottuck@users.noreply.github.com> Date: Sat, 31 Aug 2024 13:35:21 +0900 Subject: [PATCH] =?UTF-8?q?Feature/=EC=B9=9C=EA=B5=AC=20=EC=82=AD=EC=A0=9C?= =?UTF-8?q?=20=EA=B8=B0=EB=8A=A5=20(#86)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../provider/FriendsListImageProvider.dart | 10 ++ .../provider/FriendsListProvider.dart | 109 ++++++++---- .../widget/BottomSheetButtonWidget.dart | 10 +- .../friendsList/widget/FriendBottomSheet.dart | 165 ++++++++++-------- .../widget/FriendBottomSheetLauncher.dart | 26 +++ .../widget/FriendsListViewWidget.dart | 37 +--- .../friendsList/widget/PopupMenuItem.dart | 18 ++ .../widget/ShimmerSkeletonLoader.dart | 50 ++++++ .../provider/UserReportBottomSheetWidget.dart | 15 +- lib/utils/AppStrings.dart | 6 + pubspec.yaml | 1 + 11 files changed, 302 insertions(+), 145 deletions(-) create mode 100644 lib/feature/friendsList/provider/FriendsListImageProvider.dart create mode 100644 lib/feature/friendsList/widget/FriendBottomSheetLauncher.dart create mode 100644 lib/feature/friendsList/widget/PopupMenuItem.dart create mode 100644 lib/feature/friendsList/widget/ShimmerSkeletonLoader.dart diff --git a/lib/feature/friendsList/provider/FriendsListImageProvider.dart b/lib/feature/friendsList/provider/FriendsListImageProvider.dart new file mode 100644 index 00000000..c190a640 --- /dev/null +++ b/lib/feature/friendsList/provider/FriendsListImageProvider.dart @@ -0,0 +1,10 @@ +import 'package:firebase_storage/firebase_storage.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +// 친구목록 이미지 URL을 제공하는 Provider +final friendsListImageProvider = +FutureProvider.family((ref, imageName) async { + final storageRef = FirebaseStorage.instance.ref('profileimage/$imageName'); + final downloadUrl = await storageRef.getDownloadURL(); + return downloadUrl; +}); \ No newline at end of file diff --git a/lib/feature/friendsList/provider/FriendsListProvider.dart b/lib/feature/friendsList/provider/FriendsListProvider.dart index 1101265c..a20adb87 100644 --- a/lib/feature/friendsList/provider/FriendsListProvider.dart +++ b/lib/feature/friendsList/provider/FriendsListProvider.dart @@ -1,10 +1,14 @@ -import 'package:blueberry_flutter_template/utils/Talker.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_storage/firebase_storage.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:cloud_firestore/cloud_firestore.dart'; + import '../../../model/FriendModel.dart'; +import '../../../utils/AppStrings.dart'; +import '../../../utils/Talker.dart'; +import '../../userreport/provider/UserReportBottomSheetWidget.dart'; -// 친구 목록을 제공하는 Provider +// 친구목록을 제공하는 Provider final friendsListProvider = StreamProvider>((ref) { final firestore = FirebaseFirestore.instance; const userId = 'eztqDqrvEXDc8nqnnrB8'; // 로그인을 가정한 임시 유저 ID @@ -14,35 +18,76 @@ final friendsListProvider = StreamProvider>((ref) { .doc(userId) .collection('friends') .snapshots() - .asyncMap((snapshot) => Future.wait(snapshot.docs.map((doc) async { - final userID = doc['userID'] as String; - talker.info('Fetching data for userID: $userID'); - - final userDoc = - await firestore.collection('users_test').doc(userID).get(); - - if (userDoc.exists) { - talker.info('User data found: ${userDoc.data()}'); - return FriendModel.fromJson(userDoc.data()!); - } else { - talker.warning('User data not found for userID: $userID'); - throw Exception('User data not found for userID: $userID'); - } - }).toList())); + .asyncMap((snapshot) async { + final friendModels = await Future.wait(snapshot.docs.map((doc) async { + final userID = doc['userID'] as String; + final userDoc = await firestore.collection('users_test').doc(userID).get(); + + if (userDoc.exists) { + return FriendModel.fromJson(userDoc.data()!); + } else { + throw Exception(AppStrings.userNotFoundErrorMessage); + } + }).toList()); + + return friendModels; + }); }); -// 친구목록 이미지 URL을 제공하는 Provider -final friendsListImageProvider = - FutureProvider.family((ref, imageName) async { - try { - final storageRef = FirebaseStorage.instance.ref('profileimage/$imageName'); - final downloadUrl = await storageRef.getDownloadURL(); - - talker.info('Download URL for image $imageName: $downloadUrl'); - return downloadUrl; - } catch (e, stacktrace) { - talker.error( - 'Failed to fetch download URL for image $imageName', e, stacktrace); - rethrow; - } +final deleteFriendProvider = + Provider Function(BuildContext, FriendModel)>((ref) { + return (BuildContext context, FriendModel friend) async { + final firestore = FirebaseFirestore.instance; + const userId = 'eztqDqrvEXDc8nqnnrB8'; // 로그인을 가정한 임시 유저 ID + + await firestore + .collection('users_test') + .doc(userId) + .collection('friends') + .doc(friend.userID) + .delete(); + + ref.invalidate(friendsListProvider); + + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text(AppStrings.friendDeleteSuccessMessage)), + ); + } + }; }); + + + +// ui 팝업 메뉴 선택시 처리하는 함수 +void handleMenuSelection( + BuildContext context, WidgetRef ref, int value, FriendModel friend) async { + switch (value) { + case 1: + // 삭제 + final deleteFriend = ref.read(deleteFriendProvider); + Navigator.of(context).pop(); + await deleteFriend(context, friend); + break; + case 2: + Navigator.of(context).pop(); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('차단 기능이 아직 구현되지 않았습니다.')), + ); + } + break; + case 3: + // 신고 + Navigator.of(context).pop(); + showModalBottomSheet( + context: context, + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(25.0)), + ), + builder: (context) => UserReportBottomSheetWidget(friend: friend), + ); + break; + } +} diff --git a/lib/feature/friendsList/widget/BottomSheetButtonWidget.dart b/lib/feature/friendsList/widget/BottomSheetButtonWidget.dart index 749e1860..4ee12622 100644 --- a/lib/feature/friendsList/widget/BottomSheetButtonWidget.dart +++ b/lib/feature/friendsList/widget/BottomSheetButtonWidget.dart @@ -14,11 +14,15 @@ class BottomSheetButtonWidget extends StatelessWidget { Widget build(BuildContext context) { return ElevatedButton( style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(12), + ), + minimumSize: const Size(140, 50), + textStyle: const TextStyle( + fontSize: 16, // 더 큰 텍스트 크기 + fontWeight: FontWeight.bold, ), - minimumSize: const Size(120, 40), // 버튼 크기 ), onPressed: onPressed, child: Text(text), diff --git a/lib/feature/friendsList/widget/FriendBottomSheet.dart b/lib/feature/friendsList/widget/FriendBottomSheet.dart index e5b3848f..abd8d083 100644 --- a/lib/feature/friendsList/widget/FriendBottomSheet.dart +++ b/lib/feature/friendsList/widget/FriendBottomSheet.dart @@ -1,11 +1,13 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../../model/FriendModel.dart'; -import '../../../utils/AppStrings.dart'; -import '../../userreport/provider/UserReportBottomSheetWidget.dart'; +import '../provider/FriendsListProvider.dart'; import 'BottomSheetButtonWidget.dart'; +import 'PopupMenuItem.dart'; +import '../../../utils/AppStrings.dart'; class FriendBottomSheetWidget extends StatelessWidget { final FriendModel friend; @@ -16,88 +18,97 @@ class FriendBottomSheetWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - height: 260, // 바텀 시트 높이 수정 부분 - decoration: BoxDecoration( - color: Colors.grey[200], - borderRadius: const BorderRadius.vertical( - top: Radius.circular(25.0), - ), - ), - padding: const EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.only(top: 10.0), - child: Row( - children: [ - Container( - width: 120, - height: 120, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all(color: Colors.white, width: 4), - image: DecorationImage( - image: CachedNetworkImageProvider(imageUrl), - fit: BoxFit.cover, - ), - ), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(friend.name, - style: const TextStyle( - fontWeight: FontWeight.bold, fontSize: 20)), - const SizedBox(height: 8), - Text(friend.status, style: const TextStyle(fontSize: 16)), - ], - ), - ), - ], + return Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: SingleChildScrollView( + child: Container( + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: const BorderRadius.vertical( + top: Radius.circular(25.0), ), ), - const SizedBox(height: 24), - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, + child: Column( + mainAxisSize: MainAxisSize.min, children: [ - BottomSheetButtonWidget( - onPressed: () { - Navigator.of(context).pop(); - GoRouter.of(context).push('/chat'); - }, - text: AppStrings.chatButton, - ), - BottomSheetButtonWidget( - onPressed: () { - Navigator.of(context).pop(); - GoRouter.of(context) - .push('/userdetail'); // 현재 임의 경로 사용 중 수정 필요 - }, - text: AppStrings.profileButton, - ), - BottomSheetButtonWidget( - onPressed: () { - Navigator.of(context).pop(); - showModalBottomSheet( - context: context, - isScrollControlled: true, - shape: const RoundedRectangleBorder( - borderRadius: - BorderRadius.vertical(top: Radius.circular(25.0)), + Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + Container( + width: 120, + height: 120, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 4), + image: DecorationImage( + image: CachedNetworkImageProvider(imageUrl), + fit: BoxFit.cover, + ), + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(friend.name, + style: const TextStyle( + fontWeight: FontWeight.bold, fontSize: 20)), + const SizedBox(height: 8), + Text(friend.status, + style: const TextStyle(fontSize: 16)), + ], + ), ), - builder: (context) => - UserReportBottomSheetWidget(friend: friend), - ); - }, - text: AppStrings.reportButton, + Consumer( + builder: (context, ref, child) { + return PopupMenuButton( + onSelected: (value) => + handleMenuSelection(context, ref, value, friend), + itemBuilder: (context) => [ + buildPopupMenuItem( + icon: Icons.delete, + text: AppStrings.deleteButton, + value: 1), + buildPopupMenuItem( + icon: Icons.block, + text: AppStrings.blockButton, + value: 2), + buildPopupMenuItem( + icon: Icons.report, + text: AppStrings.reportButton, + value: 3), + ], + ); + }, + ), + ], + ), + ), + const SizedBox(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + BottomSheetButtonWidget( + onPressed: () { + Navigator.of(context).pop(); + GoRouter.of(context).push('/chat'); + }, + text: AppStrings.chatButton, + ), + BottomSheetButtonWidget( + onPressed: () { + Navigator.of(context).pop(); + GoRouter.of(context).push('/userdetail'); + }, + text: AppStrings.profileButton, + ), + ], ), ], ), - ], + ), ), ); } diff --git a/lib/feature/friendsList/widget/FriendBottomSheetLauncher.dart b/lib/feature/friendsList/widget/FriendBottomSheetLauncher.dart new file mode 100644 index 00000000..f3635255 --- /dev/null +++ b/lib/feature/friendsList/widget/FriendBottomSheetLauncher.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; + +import '../../../model/FriendModel.dart'; +import 'FriendBottomSheet.dart'; + +class FriendBottomSheetLauncher { + static void show({ + required BuildContext context, + required FriendModel friend, + required String imageUrl, + }) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(25.0), + ), + ), + builder: (context) => FriendBottomSheetWidget( + friend: friend, + imageUrl: imageUrl, + ), + ); + } +} diff --git a/lib/feature/friendsList/widget/FriendsListViewWidget.dart b/lib/feature/friendsList/widget/FriendsListViewWidget.dart index 73d88949..8a0565e1 100644 --- a/lib/feature/friendsList/widget/FriendsListViewWidget.dart +++ b/lib/feature/friendsList/widget/FriendsListViewWidget.dart @@ -1,9 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../../model/FriendModel.dart'; +import '../provider/FriendsListImageProvider.dart'; import '../provider/FriendsListProvider.dart'; -import 'FriendBottomSheet.dart'; +import 'FriendBottomSheetLauncher.dart'; import 'FriendListItemWidget.dart'; +import 'ShimmerSkeletonLoader.dart'; class FriendsListViewWidget extends ConsumerWidget { const FriendsListViewWidget({super.key}); @@ -39,39 +40,17 @@ class FriendsListViewWidget extends ConsumerWidget { }, ); }, - loading: () => const Center(child: CircularProgressIndicator()), - error: (error, stack) { - return const Center(child: Text('Error loading image')); - }, + loading: () => const ShimmerSkeletonLoader(), + error: (error, stack) => const Icon(Icons.error), ); }, ), ); }, - loading: () => const Center(child: CircularProgressIndicator()), - error: (error, stack) => Center(child: Text('Error: $error')), - ); - } -} - -class FriendBottomSheetLauncher { - static void show({ - required BuildContext context, - required FriendModel friend, - required String imageUrl, - }) { - showModalBottomSheet( - context: context, - isScrollControlled: true, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(25.0), - ), - ), - builder: (context) => FriendBottomSheetWidget( - friend: friend, - imageUrl: imageUrl, + loading: () => const Center( + child: CircularProgressIndicator(), ), + error: (error, stack) => Center(child: Text('Error: $error')), ); } } diff --git a/lib/feature/friendsList/widget/PopupMenuItem.dart b/lib/feature/friendsList/widget/PopupMenuItem.dart new file mode 100644 index 00000000..143179bd --- /dev/null +++ b/lib/feature/friendsList/widget/PopupMenuItem.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + +PopupMenuItem buildPopupMenuItem({ + required IconData icon, + required String text, + required int value, +}) { + return PopupMenuItem( + value: value, + child: Row( + children: [ + Icon(icon), + const SizedBox(width: 10), + Text(text), + ], + ), + ); +} diff --git a/lib/feature/friendsList/widget/ShimmerSkeletonLoader.dart b/lib/feature/friendsList/widget/ShimmerSkeletonLoader.dart new file mode 100644 index 00000000..add54adf --- /dev/null +++ b/lib/feature/friendsList/widget/ShimmerSkeletonLoader.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; + +class ShimmerSkeletonLoader extends StatelessWidget { + const ShimmerSkeletonLoader({super.key}); + + @override + Widget build(BuildContext context) { + return Shimmer.fromColors( + baseColor: Colors.grey[300]!, + highlightColor: Colors.grey[100]!, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 5.0), + child: Row( + children: [ + // 스켈레톤 아바타 + Container( + width: 50, + height: 50, + decoration: BoxDecoration( + color: Colors.grey[300], + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 16), + // 스켈레톤 텍스트 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: double.infinity, + height: 12, + color: Colors.grey[300], + ), + const SizedBox(height: 8), + Container( + width: 150, + height: 12, + color: Colors.grey[300], + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/feature/userreport/provider/UserReportBottomSheetWidget.dart b/lib/feature/userreport/provider/UserReportBottomSheetWidget.dart index 8eff8d5c..28542652 100644 --- a/lib/feature/userreport/provider/UserReportBottomSheetWidget.dart +++ b/lib/feature/userreport/provider/UserReportBottomSheetWidget.dart @@ -6,9 +6,6 @@ import '../../../model/UserReportModel.dart'; import '../../../utils/AppStrings.dart'; import '../../userreport/provider/userReportProvider.dart'; -/// UserReportBottomSheetWidget - 완성 되었습니다 -/// 8월 15일 상현 - class UserReportBottomSheetWidget extends ConsumerWidget { final FriendModel friend; final String loginUserId = 'loginUserId'; // 로그인한 사용자의 ID로 대체 @@ -17,8 +14,18 @@ class UserReportBottomSheetWidget extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final mediaQuery = MediaQuery.of(context); + final screenHeight = mediaQuery.size.height; + return Container( + height: screenHeight * 0.3, padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey[200], // 배경색 지정 + borderRadius: const BorderRadius.vertical( + top: Radius.circular(25.0), + ), + ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, @@ -28,7 +35,7 @@ class UserReportBottomSheetWidget extends ConsumerWidget { .replaceFirst('%s', friend.name), style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), - const SizedBox(height: 16), + const SizedBox(height: 14), ElevatedButton( child: const Text(AppStrings.reportReasonSpamAccount), onPressed: () => diff --git a/lib/utils/AppStrings.dart b/lib/utils/AppStrings.dart index 2cbd6311..29bdabe4 100644 --- a/lib/utils/AppStrings.dart +++ b/lib/utils/AppStrings.dart @@ -41,6 +41,12 @@ class AppStrings { static const String chatButton = '채팅'; static const String profileButton = '프로필'; static const String reportButton = '신고하기'; + static const String deleteButton = '삭제하기'; + static const String blockButton = '차단하기'; + + //FriendsListProvider.dart + static const String userNotFoundErrorMessage = '유저 데이터를 찾을 수 없습니다.'; + static const String friendDeleteSuccessMessage = '친구가 삭제되었습니다.'; //NickNameTextWidget.dart static const String nickNameTextWidgetdefaultNickName = '닉네임'; diff --git a/pubspec.yaml b/pubspec.yaml index b36a10ee..be55d143 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -43,6 +43,7 @@ dependencies: flutter_rating_bar: ^4.0.1 pinput: ^4.0.0 google_fonts: ^6.2.1 + shimmer: ^3.0.0 # Calender flutter_svg: ^2.0.10+1