From d6ef851b02e1541b9d37f3b4a99f459f9ff99d11 Mon Sep 17 00:00:00 2001 From: caadiq Date: Fri, 27 Mar 2026 18:29:22 +0900 Subject: [PATCH] =?UTF-8?q?feat(app):=20=EB=A9=A4=EB=B2=84=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=EC=9D=84=20=EC=9B=B9=EA=B3=BC=20=EB=8F=99=EC=9D=BC?= =?UTF-8?q?=ED=95=9C=202=EC=97=B4=20=EA=B7=B8=EB=A6=AC=EB=93=9C=20+=20?= =?UTF-8?q?=EB=AA=A8=EB=8B=AC=20=EB=94=94=EC=9E=90=EC=9D=B8=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PageView 카드 스와이프 → 2열 그리드 레이아웃 - 상단 썸네일 인디케이터 제거 - 카드 탭 시 모달로 상세 정보 표시 (이미지, 이름, 생일, 인스타그램) - 개별 카드 staggered 애니메이션 + 탭 scale 효과 - 컨트롤러에서 불필요한 currentIndex 상태 제거 Co-Authored-By: Claude Opus 4.6 (1M context) --- app/lib/controllers/members_controller.dart | 15 - app/lib/views/members/members_view.dart | 819 +++++++++----------- 2 files changed, 346 insertions(+), 488 deletions(-) diff --git a/app/lib/controllers/members_controller.dart b/app/lib/controllers/members_controller.dart index a625270..5caca01 100644 --- a/app/lib/controllers/members_controller.dart +++ b/app/lib/controllers/members_controller.dart @@ -11,13 +11,11 @@ import '../services/members_service.dart'; /// 멤버 상태 class MembersState { final List members; - final int currentIndex; final bool isLoading; final String? error; const MembersState({ this.members = const [], - this.currentIndex = 0, this.isLoading = true, this.error, }); @@ -25,21 +23,15 @@ class MembersState { /// 상태 복사 (불변성 유지) MembersState copyWith({ List? members, - int? currentIndex, bool? isLoading, String? error, }) { return MembersState( members: members ?? this.members, - currentIndex: currentIndex ?? this.currentIndex, isLoading: isLoading ?? this.isLoading, error: error, ); } - - /// 현재 선택된 멤버 - Member? get currentMember => - members.isNotEmpty ? members[currentIndex] : null; } /// 멤버 컨트롤러 @@ -77,13 +69,6 @@ class MembersController extends Notifier { } } - /// 현재 인덱스 변경 - void setCurrentIndex(int index) { - if (index >= 0 && index < state.members.length) { - state = state.copyWith(currentIndex: index); - } - } - /// 나이 계산 int? calculateAge(String? birthDate) { if (birthDate == null) return null; diff --git a/app/lib/views/members/members_view.dart b/app/lib/views/members/members_view.dart index 4ac9ac1..4016bd6 100644 --- a/app/lib/views/members/members_view.dart +++ b/app/lib/views/members/members_view.dart @@ -1,10 +1,9 @@ /// 멤버 화면 (MVCS의 View 레이어) /// -/// UI 렌더링과 애니메이션만 담당하고, 비즈니스 로직은 Controller에 위임합니다. +/// 웹과 동일한 2열 그리드 + 모달 디자인 library; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -21,121 +20,31 @@ class MembersView extends ConsumerStatefulWidget { ConsumerState createState() => _MembersViewState(); } -class _MembersViewState extends ConsumerState with TickerProviderStateMixin { - late PageController _pageController; - late ScrollController _indicatorScrollController; - late AnimationController _animController; - - // 애니메이션 - late Animation _indicatorOpacity; - late Animation _indicatorSlide; - late Animation _cardOpacity; - late Animation _cardSlide; - - // 인디케이터 아이템 크기 (48px 썸네일 + 12px 마진) - static const double _indicatorItemWidth = 64.0; - - // 이전 경로 저장 (탭 전환 감지용) +class _MembersViewState extends ConsumerState { String? _previousPath; - bool _animationStarted = false; - - @override - void initState() { - super.initState(); - _pageController = PageController(viewportFraction: 0.88); - _indicatorScrollController = ScrollController(); - _animController = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 800), - ); - _setupAnimations(); - } - - /// 애니메이션 설정 - void _setupAnimations() { - // 인디케이터 페이드인 (0~0.4) - _indicatorOpacity = Tween(begin: 0, end: 1).animate( - CurvedAnimation( - parent: _animController, - curve: const Interval(0, 0.4, curve: Curves.easeOut), - ), - ); - - // 인디케이터 슬라이드 (위에서 아래로) - _indicatorSlide = Tween(begin: -20, end: 0).animate( - CurvedAnimation( - parent: _animController, - curve: const Interval(0, 0.4, curve: Curves.easeOut), - ), - ); - - // 카드 페이드인 (0.2~0.7) - _cardOpacity = Tween(begin: 0, end: 1).animate( - CurvedAnimation( - parent: _animController, - curve: const Interval(0.2, 0.7, curve: Curves.easeOut), - ), - ); - - // 카드 슬라이드 (아래에서 위로) - _cardSlide = Tween(begin: 40, end: 0).animate( - CurvedAnimation( - parent: _animController, - curve: const Interval(0.2, 0.7, curve: Curves.easeOut), - ), - ); - } + // 탭 전환 시 애니메이션 재생을 위한 키 + Key _gridKey = UniqueKey(); @override void didChangeDependencies() { super.didChangeDependencies(); - // 경로 변경 감지하여 애니메이션 재생 final currentPath = GoRouterState.of(context).uri.path; if (_previousPath != null && _previousPath != currentPath && currentPath == '/members') { - _animController.reset(); - _animController.forward(); + setState(() => _gridKey = UniqueKey()); } _previousPath = currentPath; } - @override - void dispose() { - _pageController.dispose(); - _indicatorScrollController.dispose(); - _animController.dispose(); - super.dispose(); - } - - /// 인디케이터 자동 스크롤 - void _scrollIndicatorToIndex(int index) { - if (!_indicatorScrollController.hasClients) return; - - final screenWidth = MediaQuery.of(context).size.width; - // 아이템 중심 위치: 왼쪽패딩(16) + index * 아이템너비(64) + 아이템반지름(26) - const itemRadius = 26.0; // 52 / 2 - final targetOffset = (index * _indicatorItemWidth) + 16 + itemRadius - (screenWidth / 2); - final maxOffset = _indicatorScrollController.position.maxScrollExtent; - - _indicatorScrollController.animateTo( - targetOffset.clamp(0.0, maxOffset), - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - ); - } - - /// 인스타그램 열기 (딥링크 우선, 없으면 웹) Future _openInstagram(String? url) async { if (url == null) return; - // URL에서 username 추출 (instagram.com/username 형태) String? username; final uri = Uri.tryParse(url); if (uri != null && uri.pathSegments.isNotEmpty) { username = uri.pathSegments.first; } - // 인스타그램 앱 딥링크 시도 if (username != null) { final deepLink = Uri.parse('instagram://user?username=$username'); if (await canLaunchUrl(deepLink)) { @@ -144,25 +53,184 @@ class _MembersViewState extends ConsumerState with TickerProviderSt } } - // 앱이 없으면 웹으로 열기 final webUri = Uri.parse(url); if (await canLaunchUrl(webUri)) { await launchUrl(webUri, mode: LaunchMode.externalApplication); } } + /// 멤버 모달 표시 + void _showMemberModal(Member member) { + final controller = ref.read(membersProvider.notifier); + final age = controller.calculateAge(member.birthDate); + + showGeneralDialog( + context: context, + barrierDismissible: true, + barrierLabel: '닫기', + barrierColor: Colors.black.withValues(alpha: 0.6), + transitionDuration: const Duration(milliseconds: 200), + pageBuilder: (context, animation, secondaryAnimation) { + return Center( + child: ScaleTransition( + scale: CurvedAnimation( + parent: animation, + curve: Curves.easeOutCubic, + ), + child: FadeTransition( + opacity: animation, + child: Material( + color: Colors.transparent, + child: Container( + width: 264, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + ), + clipBehavior: Clip.antiAlias, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // 이미지 (3:4 비율) + AspectRatio( + aspectRatio: 3 / 4, + child: Stack( + fit: StackFit.expand, + children: [ + if (member.imageUrl != null) + CachedNetworkImage( + imageUrl: member.imageUrl!, + fit: BoxFit.cover, + placeholder: (context, url) => Container(color: Colors.grey[200]), + ) + else + Container( + color: Colors.grey[200], + child: Center( + child: Text( + member.name[0], + style: TextStyle( + fontSize: 30, + fontWeight: FontWeight.bold, + color: Colors.grey[400], + ), + ), + ), + ), + // 닫기 버튼 + Positioned( + top: 8, + right: 8, + child: GestureDetector( + onTap: () => Navigator.of(context).pop(), + child: Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.5), + shape: BoxShape.circle, + ), + child: const Icon( + Icons.close, + color: Colors.white, + size: 18, + ), + ), + ), + ), + ], + ), + ), + // 정보 영역 + Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + // 이름 + Text( + member.name, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + // 생일 + if (member.birthDate != null) ...[ + const SizedBox(height: 6), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildIcon('cake', 14, Colors.grey[500]!), + const SizedBox(width: 4), + Text( + controller.formatBirthDate(member.birthDate), + style: TextStyle( + fontSize: 14, + color: Colors.grey[500], + ), + ), + if (age != null) ...[ + const SizedBox(width: 4), + Text( + '($age세)', + style: const TextStyle( + fontSize: 14, + color: AppColors.primary, + ), + ), + ], + ], + ), + ], + // 인스타그램 버튼 + if (!member.isFormer && member.instagram != null) ...[ + const SizedBox(height: 12), + GestureDetector( + onTap: () => _openInstagram(member.instagram), + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 10), + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [Color(0xFF833AB4), Color(0xFFE1306C), Color(0xFFF77737)], + ), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildIcon('instagram', 16, Colors.white), + const SizedBox(width: 6), + const Text( + 'Instagram', + style: TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ), + ], + ], + ), + ), + ], + ), + ), + ), + ), + ), + ); + }, + ); + } + @override Widget build(BuildContext context) { final membersState = ref.watch(membersProvider); - final controller = ref.read(membersProvider.notifier); - - // 데이터 로드 완료 시 애니메이션 시작 - if (!membersState.isLoading && !_animationStarted && membersState.members.isNotEmpty) { - _animationStarted = true; - WidgetsBinding.instance.addPostFrameCallback((_) { - _animController.forward(); - }); - } if (membersState.isLoading) { return const Center( @@ -176,375 +244,26 @@ class _MembersViewState extends ConsumerState with TickerProviderSt ); } - return AnimatedBuilder( - animation: _animController, - builder: (context, child) { - return Column( - children: [ - // 상단 썸네일 인디케이터 (애니메이션 적용) - Transform.translate( - offset: Offset(0, _indicatorSlide.value), - child: Opacity( - opacity: _indicatorOpacity.value, - child: _buildThumbnailIndicator(membersState), - ), - ), + // 현재 멤버만 표시 + final currentMembers = membersState.members.where((m) => !m.isFormer).toList(); - // 메인 카드 영역 (애니메이션 적용) - Expanded( - child: Transform.translate( - offset: Offset(0, _cardSlide.value), - child: Opacity( - opacity: _cardOpacity.value, - child: PageView.builder( - controller: _pageController, - itemCount: membersState.members.length, - padEnds: true, - onPageChanged: (index) { - controller.setCurrentIndex(index); - HapticFeedback.selectionClick(); - _scrollIndicatorToIndex(index); - }, - itemBuilder: (context, index) { - return AnimatedBuilder( - animation: _pageController, - // child를 캐싱하여 매 프레임 rebuild 방지 - // RepaintBoundary로 리페인트 범위 제한 - child: RepaintBoundary( - child: _buildMemberCard(membersState.members[index], controller), - ), - builder: (context, child) { - double value = 1.0; - if (_pageController.position.haveDimensions) { - value = (_pageController.page! - index).abs(); - value = (1 - (value * 0.25)).clamp(0.0, 1.0); - } - return Transform.scale( - scale: Curves.easeOut.transform(value), - child: child, - ); - }, - ); - }, - ), - ), - ), - ), - ], - ); - }, - ); - } - - /// 멤버 카드 - Widget _buildMemberCard(Member member, MembersController controller) { - final isFormer = member.isFormer; - final age = controller.calculateAge(member.birthDate); - - return Padding( - padding: const EdgeInsets.symmetric(vertical: 12), - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(24), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.15), - blurRadius: 20, - offset: const Offset(0, 10), - ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(24), - child: Stack( - fit: StackFit.expand, - children: [ - // 배경 이미지 - if (member.imageUrl != null) - ColorFiltered( - colorFilter: isFormer - ? const ColorFilter.mode(Colors.grey, BlendMode.saturation) - : const ColorFilter.mode(Colors.transparent, BlendMode.multiply), - child: CachedNetworkImage( - imageUrl: member.imageUrl!, - fit: BoxFit.cover, - placeholder: (context, url) => Container( - color: Colors.grey[200], - child: const Center( - child: CircularProgressIndicator(color: AppColors.primary), - ), - ), - ), - ) - else - Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [Colors.grey[300]!, Colors.grey[400]!], - ), - ), - ), - - // 하단 그라데이션 오버레이 - Positioned( - left: 0, - right: 0, - bottom: 0, - child: Container( - height: 220, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Colors.transparent, - Colors.black.withValues(alpha: 0.3), - Colors.black.withValues(alpha: 0.8), - ], - stops: const [0.0, 0.4, 1.0], - ), - ), - ), - ), - - // 전 멤버 라벨 - if (isFormer) - Positioned( - top: 16, - right: 16, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.6), - borderRadius: BorderRadius.circular(20), - ), - child: const Text( - '전 멤버', - style: TextStyle( - color: Colors.white70, - fontSize: 12, - fontWeight: FontWeight.w500, - ), - ), - ), - ), - - // 멤버 정보 - Positioned( - left: 24, - right: 24, - bottom: 24, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - // 이름 - Text( - member.name, - style: const TextStyle( - fontSize: 32, - fontWeight: FontWeight.bold, - color: Colors.white, - shadows: [ - Shadow( - blurRadius: 10, - color: Colors.black45, - ), - ], - ), - ), - const SizedBox(height: 8), - - // 포지션 - if (member.position != null) - Text( - member.position!, - style: TextStyle( - fontSize: 16, - color: Colors.white.withValues(alpha: 0.9), - fontWeight: FontWeight.w500, - ), - ), - - const SizedBox(height: 12), - - // 생일 정보 - if (member.birthDate != null) - Row( - children: [ - _buildIcon('calendar', 16, Colors.white70), - const SizedBox(width: 6), - Text( - controller.formatBirthDate(member.birthDate), - style: TextStyle( - fontSize: 14, - color: Colors.white.withValues(alpha: 0.8), - ), - ), - if (age != null) ...[ - const SizedBox(width: 8), - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.2), - borderRadius: BorderRadius.circular(10), - ), - child: Text( - '$age세', - style: const TextStyle( - fontSize: 12, - color: Colors.white, - fontWeight: FontWeight.w500, - ), - ), - ), - ], - ], - ), - - // 인스타그램 버튼 - if (!isFormer && member.instagram != null) ...[ - const SizedBox(height: 16), - GestureDetector( - onTap: () => _openInstagram(member.instagram), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), - decoration: BoxDecoration( - gradient: const LinearGradient( - colors: [Color(0xFF833AB4), Color(0xFFE1306C), Color(0xFFF77737)], - ), - borderRadius: BorderRadius.circular(25), - boxShadow: [ - BoxShadow( - color: const Color(0xFFE1306C).withValues(alpha: 0.4), - blurRadius: 12, - offset: const Offset(0, 4), - ), - ], - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - _buildIcon('instagram', 18, Colors.white), - const SizedBox(width: 8), - const Text( - 'Instagram', - style: TextStyle( - color: Colors.white, - fontSize: 14, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - ), - ), - ], - ], - ), - ), - ], - ), - ), - ), - ); - } - - /// 상단 썸네일 인디케이터 - Widget _buildThumbnailIndicator(MembersState membersState) { return Container( - height: 88, - decoration: BoxDecoration( - color: Colors.white, - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.04), - blurRadius: 6, - offset: const Offset(0, 2), - ), - ], - ), - child: ListView.builder( - controller: _indicatorScrollController, - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), - itemCount: membersState.members.length, + color: const Color(0xFFF9FAFB), // bg-gray-50 + child: GridView.builder( + key: _gridKey, + padding: const EdgeInsets.all(16), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + childAspectRatio: 3 / 4, + ), + itemCount: currentMembers.length, itemBuilder: (context, index) { - final member = membersState.members[index]; - final isSelected = index == membersState.currentIndex; - final isFormer = member.isFormer; - - return GestureDetector( - onTap: () { - _pageController.animateToPage( - index, - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - ); - }, - child: Padding( - padding: const EdgeInsets.only(right: 12), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // 테두리 + 그림자를 위한 외부 컨테이너 - AnimatedContainer( - duration: const Duration(milliseconds: 200), - width: 52, - height: 52, - padding: const EdgeInsets.all(2), - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all( - color: isSelected ? AppColors.primary : Colors.grey.shade300, - width: isSelected ? 2.5 : 1.5, - ), - boxShadow: isSelected - ? [ - BoxShadow( - color: AppColors.primary.withValues(alpha: 0.35), - blurRadius: 8, - spreadRadius: 1, - ), - ] - : null, - ), - // 이미지를 담는 내부 ClipOval - child: ClipOval( - child: ColorFiltered( - colorFilter: isFormer - ? const ColorFilter.mode(Colors.grey, BlendMode.saturation) - : const ColorFilter.mode(Colors.transparent, BlendMode.multiply), - child: member.imageUrl != null - ? CachedNetworkImage( - imageUrl: member.imageUrl!, - fit: BoxFit.cover, - width: 48, - height: 48, - placeholder: (context, url) => Container(color: Colors.grey[300]), - ) - : Container( - width: 48, - height: 48, - color: Colors.grey[300], - child: Center( - child: Text( - member.name[0], - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - ), - ), - ), - ], - ), - ), + return _AnimatedMemberCard( + index: index, + member: currentMembers[index], + onTap: () => _showMemberModal(currentMembers[index]), ); }, ), @@ -554,7 +273,7 @@ class _MembersViewState extends ConsumerState with TickerProviderSt /// SVG 아이콘 빌더 Widget _buildIcon(String name, double size, Color color) { const icons = { - 'calendar': '', + 'cake': '', 'instagram': '', }; @@ -571,3 +290,157 @@ class _MembersViewState extends ConsumerState with TickerProviderSt ); } } + +/// 개별 애니메이션이 적용된 멤버 카드 +/// Framer Motion과 동일: delay index*50ms, opacity 0→1, y 20→0, tap scale 0.97 +class _AnimatedMemberCard extends StatefulWidget { + final int index; + final Member member; + final VoidCallback onTap; + + const _AnimatedMemberCard({ + required this.index, + required this.member, + required this.onTap, + }); + + @override + State<_AnimatedMemberCard> createState() => _AnimatedMemberCardState(); +} + +class _AnimatedMemberCardState extends State<_AnimatedMemberCard> + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _opacity; + late Animation _slide; + double _scale = 1.0; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 300), + ); + _opacity = Tween(begin: 0, end: 1).animate( + CurvedAnimation(parent: _controller, curve: Curves.easeOut), + ); + _slide = Tween(begin: const Offset(0, 20), end: Offset.zero).animate( + CurvedAnimation(parent: _controller, curve: Curves.easeOut), + ); + + // 개별 딜레이 (index * 50ms) + Future.delayed(Duration(milliseconds: widget.index * 50), () { + if (mounted) _controller.forward(); + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _controller, + builder: (context, child) { + return Opacity( + opacity: _opacity.value, + child: Transform.translate( + offset: _slide.value, + child: child, + ), + ); + }, + child: GestureDetector( + onTapDown: (_) => setState(() => _scale = 0.97), + onTapUp: (_) { + setState(() => _scale = 1.0); + widget.onTap(); + }, + onTapCancel: () => setState(() => _scale = 1.0), + child: AnimatedScale( + scale: _scale, + duration: const Duration(milliseconds: 100), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.1), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: Stack( + fit: StackFit.expand, + children: [ + // 이미지 + if (widget.member.imageUrl != null) + CachedNetworkImage( + imageUrl: widget.member.imageUrl!, + fit: BoxFit.cover, + placeholder: (context, url) => Container(color: Colors.grey[200]), + ) + else + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Colors.grey[200]!, Colors.grey[300]!], + ), + ), + child: Center( + child: Text( + widget.member.name[0], + style: TextStyle( + fontSize: 36, + fontWeight: FontWeight.bold, + color: Colors.grey[400], + ), + ), + ), + ), + // 하단 그라데이션 + 이름 + Positioned( + left: 0, + right: 0, + bottom: 0, + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.transparent, + Colors.black.withValues(alpha: 0.45), + ], + ), + ), + child: Text( + widget.member.name, + textAlign: TextAlign.center, + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + ), + ), + ), + ), + ); + } +}