diff --git a/app/lib/controllers/album_controller.dart b/app/lib/controllers/album_controller.dart new file mode 100644 index 0000000..c91228a --- /dev/null +++ b/app/lib/controllers/album_controller.dart @@ -0,0 +1,73 @@ +/// 앨범 컨트롤러 (MVCS의 Controller 레이어) +/// +/// 비즈니스 로직과 상태 관리를 담당합니다. +/// View는 이 Controller를 통해 데이터에 접근합니다. +library; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../models/album.dart'; +import '../services/albums_service.dart'; + +/// 앨범 상태 +class AlbumState { + final List albums; + final bool isLoading; + final String? error; + + const AlbumState({ + this.albums = const [], + this.isLoading = true, + this.error, + }); + + /// 상태 복사 (불변성 유지) + AlbumState copyWith({ + List? albums, + bool? isLoading, + String? error, + }) { + return AlbumState( + albums: albums ?? this.albums, + isLoading: isLoading ?? this.isLoading, + error: error, + ); + } +} + +/// 앨범 컨트롤러 +class AlbumController extends Notifier { + @override + AlbumState build() { + // 초기 데이터 로드 + Future.microtask(() => loadAlbums()); + return const AlbumState(); + } + + /// 앨범 목록 로드 + Future loadAlbums() async { + state = state.copyWith(isLoading: true, error: null); + + try { + final albums = await getAlbums(); + state = state.copyWith( + albums: albums, + isLoading: false, + ); + } catch (e) { + state = state.copyWith( + isLoading: false, + error: e.toString(), + ); + } + } + + /// 앨범 새로고침 + Future refresh() async { + await loadAlbums(); + } +} + +/// 앨범 Provider +final albumProvider = NotifierProvider( + AlbumController.new, +); diff --git a/app/lib/controllers/home_controller.dart b/app/lib/controllers/home_controller.dart new file mode 100644 index 0000000..b6a4558 --- /dev/null +++ b/app/lib/controllers/home_controller.dart @@ -0,0 +1,93 @@ +/// 홈 컨트롤러 (MVCS의 Controller 레이어) +/// +/// 비즈니스 로직과 상태 관리를 담당합니다. +/// View는 이 Controller를 통해 데이터에 접근합니다. +library; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../models/member.dart'; +import '../models/album.dart'; +import '../models/schedule.dart'; +import '../services/members_service.dart'; +import '../services/albums_service.dart'; +import '../services/schedules_service.dart'; + +/// 홈 상태 +class HomeState { + final List members; + final List albums; + final List schedules; + final bool isLoading; + final bool dataLoaded; + final String? error; + + const HomeState({ + this.members = const [], + this.albums = const [], + this.schedules = const [], + this.isLoading = true, + this.dataLoaded = false, + this.error, + }); + + /// 상태 복사 (불변성 유지) + HomeState copyWith({ + List? members, + List? albums, + List? schedules, + bool? isLoading, + bool? dataLoaded, + String? error, + }) { + return HomeState( + members: members ?? this.members, + albums: albums ?? this.albums, + schedules: schedules ?? this.schedules, + isLoading: isLoading ?? this.isLoading, + dataLoaded: dataLoaded ?? this.dataLoaded, + error: error, + ); + } +} + +/// 홈 컨트롤러 +class HomeController extends Notifier { + @override + HomeState build() { + // 초기 데이터 로드 + Future.microtask(() => loadData()); + return const HomeState(); + } + + /// 데이터 로드 (멤버, 앨범, 일정) + Future loadData() async { + state = state.copyWith(isLoading: true, error: null); + + try { + final results = await Future.wait([ + getActiveMembers(), + getRecentAlbums(2), + getUpcomingSchedules(3), + ]); + + state = state.copyWith( + members: results[0] as List, + albums: results[1] as List, + schedules: results[2] as List, + isLoading: false, + dataLoaded: true, + ); + } catch (e) { + state = state.copyWith( + isLoading: false, + dataLoaded: true, + error: e.toString(), + ); + } + } +} + +/// 홈 Provider +final homeProvider = NotifierProvider( + HomeController.new, +); diff --git a/app/lib/controllers/members_controller.dart b/app/lib/controllers/members_controller.dart new file mode 100644 index 0000000..a625270 --- /dev/null +++ b/app/lib/controllers/members_controller.dart @@ -0,0 +1,111 @@ +/// 멤버 컨트롤러 (MVCS의 Controller 레이어) +/// +/// 비즈니스 로직과 상태 관리를 담당합니다. +/// View는 이 Controller를 통해 데이터에 접근합니다. +library; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../models/member.dart'; +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, + }); + + /// 상태 복사 (불변성 유지) + 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; +} + +/// 멤버 컨트롤러 +class MembersController extends Notifier { + @override + MembersState build() { + // 초기 데이터 로드 + Future.microtask(() => loadMembers()); + return const MembersState(); + } + + /// 멤버 목록 로드 + Future loadMembers() async { + state = state.copyWith(isLoading: true, error: null); + + try { + final members = await getMembers(); + // 현재 멤버 먼저, 전 멤버 나중에 정렬 + members.sort((a, b) { + if (a.isFormer != b.isFormer) { + return a.isFormer ? 1 : -1; + } + return 0; + }); + + state = state.copyWith( + members: members, + isLoading: false, + ); + } catch (e) { + state = state.copyWith( + isLoading: false, + error: e.toString(), + ); + } + } + + /// 현재 인덱스 변경 + 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; + final birth = DateTime.tryParse(birthDate); + if (birth == null) return null; + final today = DateTime.now(); + int age = today.year - birth.year; + if (today.month < birth.month || + (today.month == birth.month && today.day < birth.day)) { + age--; + } + return age; + } + + /// 생일 포맷팅 + String formatBirthDate(String? birthDate) { + if (birthDate == null) return ''; + return birthDate.substring(0, 10).replaceAll('-', '.'); + } +} + +/// 멤버 Provider +final membersProvider = NotifierProvider( + MembersController.new, +); diff --git a/app/lib/views/album/album_view.dart b/app/lib/views/album/album_view.dart index d4cb55f..a0a1df4 100644 --- a/app/lib/views/album/album_view.dart +++ b/app/lib/views/album/album_view.dart @@ -1,29 +1,30 @@ -/// 앨범 목록 화면 +/// 앨범 목록 화면 (MVCS의 View 레이어) +/// +/// UI 렌더링과 애니메이션만 담당하고, 비즈니스 로직은 Controller에 위임합니다. library; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:lucide_icons/lucide_icons.dart'; import '../../core/constants.dart'; import '../../models/album.dart'; -import '../../services/albums_service.dart'; +import '../../controllers/album_controller.dart'; -class AlbumView extends StatefulWidget { +class AlbumView extends ConsumerStatefulWidget { const AlbumView({super.key}); @override - State createState() => _AlbumViewState(); + ConsumerState createState() => _AlbumViewState(); } -class _AlbumViewState extends State { - late Future> _albumsFuture; +class _AlbumViewState extends ConsumerState { bool _initialLoadComplete = false; @override void initState() { super.initState(); - _albumsFuture = getAlbums(); // 초기 애니메이션 시간 후에는 새로 생성되는 카드에 애니메이션 적용 안함 Future.delayed(const Duration(milliseconds: 600), () { if (mounted) { @@ -34,69 +35,62 @@ class _AlbumViewState extends State { @override Widget build(BuildContext context) { - return FutureBuilder>( - future: _albumsFuture, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center( - child: CircularProgressIndicator( - color: AppColors.primary, + final albumState = ref.watch(albumProvider); + + if (albumState.isLoading) { + return const Center( + child: CircularProgressIndicator( + color: AppColors.primary, + ), + ); + } + + if (albumState.error != null) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + LucideIcons.alertCircle, + size: 48, + color: AppColors.textTertiary, ), - ); - } - - if (snapshot.hasError) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon( - LucideIcons.alertCircle, - size: 48, - color: AppColors.textTertiary, - ), - const SizedBox(height: 16), - Text( - '앨범을 불러오는데 실패했습니다', - style: TextStyle( - fontSize: 14, - color: AppColors.textSecondary, - ), - ), - const SizedBox(height: 16), - TextButton( - onPressed: () { - setState(() { - _albumsFuture = getAlbums(); - }); - }, - child: const Text('다시 시도'), - ), - ], + const SizedBox(height: 16), + Text( + '앨범을 불러오는데 실패했습니다', + style: TextStyle( + fontSize: 14, + color: AppColors.textSecondary, + ), ), - ); - } + const SizedBox(height: 16), + TextButton( + onPressed: () { + ref.read(albumProvider.notifier).refresh(); + }, + child: const Text('다시 시도'), + ), + ], + ), + ); + } - final albums = snapshot.data ?? []; - - return GridView.builder( - padding: const EdgeInsets.all(16), - clipBehavior: Clip.none, - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - crossAxisSpacing: 16, - mainAxisSpacing: 16, - childAspectRatio: 0.75, - ), - itemCount: albums.length, - itemBuilder: (context, index) { - final album = albums[index]; - return _AlbumCard( - album: album, - index: index, - skipAnimation: _initialLoadComplete, - ); - }, + return GridView.builder( + padding: const EdgeInsets.all(16), + clipBehavior: Clip.none, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + childAspectRatio: 0.75, + ), + itemCount: albumState.albums.length, + itemBuilder: (context, index) { + final album = albumState.albums[index]; + return _AlbumCard( + album: album, + index: index, + skipAnimation: _initialLoadComplete, ); }, ); diff --git a/app/lib/views/home/home_view.dart b/app/lib/views/home/home_view.dart index e0d22b7..3aa7918 100644 --- a/app/lib/views/home/home_view.dart +++ b/app/lib/views/home/home_view.dart @@ -1,32 +1,25 @@ -/// 홈 화면 - 모바일 웹과 픽셀 단위까지 동일한 디자인 + 애니메이션 +/// 홈 화면 (MVCS의 View 레이어) +/// +/// UI 렌더링과 애니메이션만 담당하고, 비즈니스 로직은 Controller에 위임합니다. library; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter_svg/flutter_svg.dart'; import '../../core/constants.dart'; -import '../../models/member.dart'; -import '../../models/album.dart'; import '../../models/schedule.dart'; -import '../../services/members_service.dart'; -import '../../services/albums_service.dart'; -import '../../services/schedules_service.dart'; +import '../../controllers/home_controller.dart'; -class HomeView extends StatefulWidget { +class HomeView extends ConsumerStatefulWidget { const HomeView({super.key}); @override - State createState() => _HomeViewState(); + ConsumerState createState() => _HomeViewState(); } -class _HomeViewState extends State with TickerProviderStateMixin { - List _members = []; - List _albums = []; - List _schedules = []; - bool _isLoading = true; - bool _dataLoaded = false; - +class _HomeViewState extends ConsumerState with TickerProviderStateMixin { // 애니메이션 컨트롤러 late AnimationController _animController; @@ -43,12 +36,12 @@ class _HomeViewState extends State with TickerProviderStateMixin { // 현재 경로 추적 (홈 탭 선택 시 애니메이션 재시작용) String? _previousPath; + bool _animationStarted = false; @override void initState() { super.initState(); _setupAnimations(); - _loadData(); } void _setupAnimations() { @@ -146,9 +139,10 @@ class _HomeViewState extends State with TickerProviderStateMixin { super.didChangeDependencies(); // go_router에서 현재 경로 감지 final currentPath = GoRouterState.of(context).uri.path; + final homeState = ref.read(homeProvider); // 다른 탭에서 홈('/')으로 돌아왔을 때 애니메이션 재시작 - if (_previousPath != null && _previousPath != '/' && currentPath == '/' && _dataLoaded) { + if (_previousPath != null && _previousPath != '/' && currentPath == '/' && homeState.dataLoaded) { _startAnimations(); } _previousPath = currentPath; @@ -160,36 +154,19 @@ class _HomeViewState extends State with TickerProviderStateMixin { super.dispose(); } - Future _loadData() async { - try { - final results = await Future.wait([ - getActiveMembers(), - getRecentAlbums(2), - getUpcomingSchedules(3), - ]); - - setState(() { - _members = results[0] as List; - _albums = results[1] as List; - _schedules = results[2] as List; - _isLoading = false; - _dataLoaded = true; - }); - - // 데이터 로드 완료 후 애니메이션 시작 - _startAnimations(); - } catch (e) { - setState(() { - _isLoading = false; - _dataLoaded = true; - }); - _startAnimations(); - } - } - @override Widget build(BuildContext context) { - if (_isLoading) { + final homeState = ref.watch(homeProvider); + + // 데이터 로드 완료 시 애니메이션 시작 + if (homeState.dataLoaded && !_animationStarted) { + _animationStarted = true; + WidgetsBinding.instance.addPostFrameCallback((_) { + _startAnimations(); + }); + } + + if (homeState.isLoading) { return const Center( child: CircularProgressIndicator(color: AppColors.primary), ); @@ -202,9 +179,9 @@ class _HomeViewState extends State with TickerProviderStateMixin { child: Column( children: [ _buildHeroSection(), - _buildMembersSection(), - _buildAlbumsSection(), - _buildSchedulesSection(), + _buildMembersSection(homeState), + _buildAlbumsSection(homeState), + _buildSchedulesSection(homeState), ], ), ); @@ -305,7 +282,7 @@ class _HomeViewState extends State with TickerProviderStateMixin { } /// 멤버 섹션 - px-4(16px) py-6(24px) - Widget _buildMembersSection() { + Widget _buildMembersSection(HomeState homeState) { return Opacity( opacity: _membersSectionOpacity.value, child: Transform.translate( @@ -319,7 +296,7 @@ class _HomeViewState extends State with TickerProviderStateMixin { const SizedBox(height: 16), // grid-cols-5 gap-2(8px) Row( - children: _members.asMap().entries.map((entry) { + children: homeState.members.asMap().entries.map((entry) { final index = entry.key; final member = entry.value; // 멤버 아이템: delay 0.4+index*0.05s, duration 0.3s @@ -361,7 +338,7 @@ class _HomeViewState extends State with TickerProviderStateMixin { ? CachedNetworkImage( imageUrl: member.imageUrl!, fit: BoxFit.cover, - placeholder: (_, __) => Container(color: Colors.grey[200]), + placeholder: (context, url) => Container(color: Colors.grey[200]), ) : null, ), @@ -389,7 +366,7 @@ class _HomeViewState extends State with TickerProviderStateMixin { } /// 앨범 섹션 - px-4(16px) py-6(24px) - Widget _buildAlbumsSection() { + Widget _buildAlbumsSection(HomeState homeState) { return Opacity( opacity: _albumsSectionOpacity.value, child: Transform.translate( @@ -403,7 +380,7 @@ class _HomeViewState extends State with TickerProviderStateMixin { const SizedBox(height: 16), // grid-cols-2 gap-3(12px) Row( - children: _albums.asMap().entries.map((entry) { + children: homeState.albums.asMap().entries.map((entry) { final index = entry.key; final album = entry.value; // 앨범 아이템: delay 0.6+index*0.1s, duration 0.3s @@ -457,7 +434,7 @@ class _HomeViewState extends State with TickerProviderStateMixin { ? CachedNetworkImage( imageUrl: album.coverThumbUrl!, fit: BoxFit.cover, - placeholder: (_, __) => Container(color: Colors.grey[200]), + placeholder: (context, url) => Container(color: Colors.grey[200]), ) : Container(color: Colors.grey[200]), ), @@ -500,7 +477,7 @@ class _HomeViewState extends State with TickerProviderStateMixin { } /// 일정 섹션 - px-4(16px) py-4(16px) - Widget _buildSchedulesSection() { + Widget _buildSchedulesSection(HomeState homeState) { return Opacity( opacity: _schedulesSectionOpacity.value, child: Transform.translate( @@ -512,7 +489,7 @@ class _HomeViewState extends State with TickerProviderStateMixin { // mb-4(16px) _buildSectionHeader('다가오는 일정', () => context.go('/schedule')), const SizedBox(height: 16), - if (_schedules.isEmpty) + if (homeState.schedules.isEmpty) Container( padding: const EdgeInsets.symmetric(vertical: 32), child: Text( @@ -523,7 +500,7 @@ class _HomeViewState extends State with TickerProviderStateMixin { else // space-y-3(12px) Column( - children: _schedules.asMap().entries.map((entry) { + children: homeState.schedules.asMap().entries.map((entry) { final index = entry.key; final schedule = entry.value; // 일정 아이템: delay 0.8+index*0.1s, duration 0.3s, x -20→0 @@ -544,7 +521,7 @@ class _HomeViewState extends State with TickerProviderStateMixin { ); return Padding( - padding: EdgeInsets.only(bottom: index < _schedules.length - 1 ? 12 : 0), + padding: EdgeInsets.only(bottom: index < homeState.schedules.length - 1 ? 12 : 0), child: Opacity( opacity: itemOpacity.value, child: Transform.translate( diff --git a/app/lib/views/members/members_view.dart b/app/lib/views/members/members_view.dart index c212c9e..032f9fb 100644 --- a/app/lib/views/members/members_view.dart +++ b/app/lib/views/members/members_view.dart @@ -1,27 +1,27 @@ -/// 멤버 화면 - 카드 스와이프 스타일 +/// 멤버 화면 (MVCS의 View 레이어) +/// +/// UI 렌더링과 애니메이션만 담당하고, 비즈니스 로직은 Controller에 위임합니다. 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'; import 'package:url_launcher/url_launcher.dart'; import 'package:go_router/go_router.dart'; import '../../core/constants.dart'; import '../../models/member.dart'; -import '../../services/members_service.dart'; +import '../../controllers/members_controller.dart'; -class MembersView extends StatefulWidget { +class MembersView extends ConsumerStatefulWidget { const MembersView({super.key}); @override - State createState() => _MembersViewState(); + ConsumerState createState() => _MembersViewState(); } -class _MembersViewState extends State with TickerProviderStateMixin { - List _members = []; - bool _isLoading = true; - int _currentIndex = 0; +class _MembersViewState extends ConsumerState with TickerProviderStateMixin { late PageController _pageController; late ScrollController _indicatorScrollController; late AnimationController _animController; @@ -37,6 +37,7 @@ class _MembersViewState extends State with TickerProviderStateMixin // 이전 경로 저장 (탭 전환 감지용) String? _previousPath; + bool _animationStarted = false; @override void initState() { @@ -48,7 +49,6 @@ class _MembersViewState extends State with TickerProviderStateMixin duration: const Duration(milliseconds: 800), ); _setupAnimations(); - _loadData(); } /// 애니메이션 설정 @@ -123,46 +123,6 @@ class _MembersViewState extends State with TickerProviderStateMixin ); } - Future _loadData() async { - try { - final members = await getMembers(); - // 현재 멤버 먼저, 전 멤버 나중에 정렬 - members.sort((a, b) { - if (a.isFormer != b.isFormer) { - return a.isFormer ? 1 : -1; - } - return 0; - }); - - setState(() { - _members = members; - _isLoading = false; - }); - _animController.forward(); - } catch (e) { - setState(() => _isLoading = false); - } - } - - /// 나이 계산 - int? _calculateAge(String? birthDate) { - if (birthDate == null) return null; - final birth = DateTime.tryParse(birthDate); - if (birth == null) return null; - final today = DateTime.now(); - int age = today.year - birth.year; - if (today.month < birth.month || - (today.month == birth.month && today.day < birth.day)) { - age--; - } - return age; - } - - /// 생일 포맷팅 - String _formatBirthDate(String? birthDate) { - if (birthDate == null) return ''; - return birthDate.substring(0, 10).replaceAll('-', '.'); - } /// 인스타그램 열기 (딥링크 우선, 없으면 웹) Future _openInstagram(String? url) async { @@ -193,13 +153,24 @@ class _MembersViewState extends State with TickerProviderStateMixin @override Widget build(BuildContext context) { - if (_isLoading) { + 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( child: CircularProgressIndicator(color: AppColors.primary), ); } - if (_members.isEmpty) { + if (membersState.members.isEmpty) { return const Center( child: Text('멤버 정보가 없습니다', style: TextStyle(color: AppColors.textSecondary)), ); @@ -215,7 +186,7 @@ class _MembersViewState extends State with TickerProviderStateMixin offset: Offset(0, _indicatorSlide.value), child: Opacity( opacity: _indicatorOpacity.value, - child: _buildThumbnailIndicator(), + child: _buildThumbnailIndicator(membersState), ), ), @@ -227,10 +198,10 @@ class _MembersViewState extends State with TickerProviderStateMixin opacity: _cardOpacity.value, child: PageView.builder( controller: _pageController, - itemCount: _members.length, + itemCount: membersState.members.length, padEnds: true, onPageChanged: (index) { - setState(() => _currentIndex = index); + controller.setCurrentIndex(index); HapticFeedback.selectionClick(); _scrollIndicatorToIndex(index); }, @@ -245,7 +216,7 @@ class _MembersViewState extends State with TickerProviderStateMixin } return Transform.scale( scale: Curves.easeOut.transform(value), - child: _buildMemberCard(_members[index], index), + child: _buildMemberCard(membersState.members[index], controller), ); }, ); @@ -261,9 +232,9 @@ class _MembersViewState extends State with TickerProviderStateMixin } /// 멤버 카드 - Widget _buildMemberCard(Member member, int index) { + Widget _buildMemberCard(Member member, MembersController controller) { final isFormer = member.isFormer; - final age = _calculateAge(member.birthDate); + final age = controller.calculateAge(member.birthDate); return Padding( padding: const EdgeInsets.symmetric(vertical: 12), @@ -292,7 +263,7 @@ class _MembersViewState extends State with TickerProviderStateMixin child: CachedNetworkImage( imageUrl: member.imageUrl!, fit: BoxFit.cover, - placeholder: (_, _) => Container( + placeholder: (context, url) => Container( color: Colors.grey[200], child: const Center( child: CircularProgressIndicator(color: AppColors.primary), @@ -401,7 +372,7 @@ class _MembersViewState extends State with TickerProviderStateMixin _buildIcon('calendar', 16, Colors.white70), const SizedBox(width: 6), Text( - _formatBirthDate(member.birthDate), + controller.formatBirthDate(member.birthDate), style: TextStyle( fontSize: 14, color: Colors.white.withValues(alpha: 0.8), @@ -477,7 +448,7 @@ class _MembersViewState extends State with TickerProviderStateMixin } /// 상단 썸네일 인디케이터 - Widget _buildThumbnailIndicator() { + Widget _buildThumbnailIndicator(MembersState membersState) { return Container( height: 88, decoration: BoxDecoration( @@ -494,10 +465,10 @@ class _MembersViewState extends State with TickerProviderStateMixin controller: _indicatorScrollController, scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), - itemCount: _members.length, + itemCount: membersState.members.length, itemBuilder: (context, index) { - final member = _members[index]; - final isSelected = index == _currentIndex; + final member = membersState.members[index]; + final isSelected = index == membersState.currentIndex; final isFormer = member.isFormer; return GestureDetector( @@ -547,7 +518,7 @@ class _MembersViewState extends State with TickerProviderStateMixin fit: BoxFit.cover, width: 48, height: 48, - placeholder: (_, _) => Container(color: Colors.grey[300]), + placeholder: (context, url) => Container(color: Colors.grey[300]), ) : Container( width: 48,