414 lines
13 KiB
Dart
414 lines
13 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:intl/intl.dart';
|
|
|
|
import '../../services/auth_service.dart';
|
|
import '../../services/club_service.dart';
|
|
import '../../services/member_service.dart';
|
|
import '../../services/event_service.dart';
|
|
import '../../models/club_model.dart';
|
|
import 'club_settings_screen.dart';
|
|
import '../../theme/badges.dart';
|
|
|
|
class DashboardScreen extends StatefulWidget {
|
|
const DashboardScreen({super.key});
|
|
|
|
@override
|
|
State<DashboardScreen> createState() => _DashboardScreenState();
|
|
}
|
|
|
|
class _DashboardScreenState extends State<DashboardScreen> {
|
|
bool _isLoading = false;
|
|
bool _isInit = false;
|
|
|
|
@override
|
|
void didChangeDependencies() {
|
|
super.didChangeDependencies();
|
|
if (!_isInit) {
|
|
_loadDashboardData();
|
|
_isInit = true;
|
|
}
|
|
}
|
|
|
|
Future<void> _loadDashboardData() async {
|
|
setState(() {
|
|
_isLoading = true;
|
|
});
|
|
|
|
// await 이전에 messenger 캡처 (catch에서 사용)
|
|
final messenger = ScaffoldMessenger.of(context);
|
|
// await 이전에 서비스 인스턴스 캡처
|
|
final authService = Provider.of<AuthService>(context, listen: false);
|
|
final clubService = Provider.of<ClubService>(context, listen: false);
|
|
final memberService = Provider.of<MemberService>(context, listen: false);
|
|
final eventService = Provider.of<EventService>(context, listen: false);
|
|
|
|
try {
|
|
// 클럽 정보 로드
|
|
if (clubService.currentClub == null) {
|
|
await clubService.initialize(authService.token!);
|
|
}
|
|
|
|
// 회원 및 이벤트 서비스 초기화
|
|
if (clubService.currentClub != null) {
|
|
memberService.initialize(authService.token!);
|
|
eventService.initialize(authService.token!);
|
|
|
|
// 회원 및 이벤트 데이터 로드
|
|
await Future.wait([
|
|
memberService.fetchClubMembers(),
|
|
eventService.fetchClubEvents(),
|
|
]);
|
|
}
|
|
} catch (e) {
|
|
// 오류 처리
|
|
messenger.showSnackBar(
|
|
SnackBar(content: Text('데이터 로드 중 오류가 발생했습니다: $e')),
|
|
);
|
|
} finally {
|
|
if (mounted) {
|
|
setState(() {
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
body: _isLoading
|
|
? const Center(child: CircularProgressIndicator())
|
|
: RefreshIndicator(
|
|
onRefresh: _loadDashboardData,
|
|
child: Consumer3<ClubService, MemberService, EventService>(
|
|
builder: (context, clubService, memberService, eventService, _) {
|
|
final club = clubService.currentClub;
|
|
|
|
if (club == null) {
|
|
return const Center(
|
|
child: Text('클럽 정보를 불러올 수 없습니다'),
|
|
);
|
|
}
|
|
|
|
return SingleChildScrollView(
|
|
physics: const AlwaysScrollableScrollPhysics(),
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// 클럽 정보 카드
|
|
_buildClubInfoCard(club),
|
|
const SizedBox(height: 16),
|
|
|
|
// 통계 카드들
|
|
_buildStatisticsCards(
|
|
memberCount: memberService.members.length,
|
|
eventCount: eventService.events.length,
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// 최근 이벤트 목록
|
|
_buildRecentEventsList(eventService),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildClubInfoCard(Club club) {
|
|
return Card(
|
|
elevation: 2,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Expanded(
|
|
child: Row(
|
|
children: [
|
|
if (club.logo != null)
|
|
ClipRRect(
|
|
borderRadius: BorderRadius.circular(8),
|
|
child: Image.network(
|
|
club.logo!,
|
|
width: 60,
|
|
height: 60,
|
|
fit: BoxFit.cover,
|
|
errorBuilder: (context, error, stackTrace) {
|
|
return Container(
|
|
width: 60,
|
|
height: 60,
|
|
color: Colors.grey[300],
|
|
child: const Icon(Icons.sports_golf),
|
|
);
|
|
},
|
|
),
|
|
)
|
|
else
|
|
Container(
|
|
width: 60,
|
|
height: 60,
|
|
decoration: BoxDecoration(
|
|
color: Colors.blue[100],
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: const Icon(
|
|
Icons.sports_golf,
|
|
size: 30,
|
|
color: Colors.blue,
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
club.name,
|
|
style: const TextStyle(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
if (club.description != null)
|
|
Text(
|
|
club.description!,
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: Colors.grey[600],
|
|
),
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.settings),
|
|
tooltip: '클럽 설정',
|
|
onPressed: () {
|
|
Navigator.of(context).pushNamed(ClubSettingsScreen.routeName);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
if (club.address != null)
|
|
_buildInfoRow(Icons.location_on, club.address!),
|
|
if (club.phone != null)
|
|
_buildInfoRow(Icons.phone, club.phone!),
|
|
if (club.email != null)
|
|
_buildInfoRow(Icons.email, club.email!),
|
|
if (club.website != null)
|
|
_buildInfoRow(Icons.language, club.website!),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildInfoRow(IconData icon, String text) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 8.0),
|
|
child: Row(
|
|
children: [
|
|
Icon(icon, size: 16, color: Colors.grey[600]),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
text,
|
|
style: TextStyle(fontSize: 14, color: Colors.grey[800]),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildStatisticsCards({required int memberCount, required int eventCount}) {
|
|
return Row(
|
|
children: [
|
|
Expanded(
|
|
child: _buildStatCard(
|
|
title: '회원',
|
|
value: memberCount.toString(),
|
|
icon: Icons.people,
|
|
color: Colors.blue,
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: _buildStatCard(
|
|
title: '이벤트',
|
|
value: eventCount.toString(),
|
|
icon: Icons.event,
|
|
color: Colors.orange,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildStatCard({
|
|
required String title,
|
|
required String value,
|
|
required IconData icon,
|
|
required Color color,
|
|
}) {
|
|
return Card(
|
|
elevation: 2,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
children: [
|
|
Icon(icon, size: 32, color: color),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
value,
|
|
style: TextStyle(
|
|
fontSize: 24,
|
|
fontWeight: FontWeight.bold,
|
|
color: color,
|
|
),
|
|
),
|
|
Text(
|
|
title,
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: Colors.grey[600],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildRecentEventsList(EventService eventService) {
|
|
final events = eventService.events;
|
|
|
|
if (events.isEmpty) {
|
|
return const Card(
|
|
child: Padding(
|
|
padding: EdgeInsets.all(16.0),
|
|
child: Center(
|
|
child: Text('예정된 이벤트가 없습니다'),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// 날짜 기준으로 정렬 (가까운 날짜순)
|
|
final sortedEvents = [...events];
|
|
sortedEvents.sort((a, b) => a.startDate.compareTo(b.startDate));
|
|
|
|
// 앞으로 진행될 이벤트만 필터링 (최대 3개)
|
|
final upcomingEvents = sortedEvents
|
|
.where((event) => event.startDate.isAfter(DateTime.now()))
|
|
.take(3)
|
|
.toList();
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
const Text(
|
|
'다가오는 이벤트',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
TextButton(
|
|
onPressed: () {
|
|
// 이벤트 목록 화면으로 이동 (추후 구현)
|
|
},
|
|
child: const Text('모두 보기'),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
...upcomingEvents.map((event) => _buildEventCard(event)),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildEventCard(event) {
|
|
final dateFormat = DateFormat('yyyy년 MM월 dd일 HH:mm');
|
|
|
|
return Card(
|
|
margin: const EdgeInsets.only(bottom: 12),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: ListTile(
|
|
contentPadding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 8,
|
|
),
|
|
title: Text(
|
|
event.title,
|
|
style: const TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
subtitle: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const SizedBox(height: 4),
|
|
if (event.status != null) ...[
|
|
Wrap(
|
|
spacing: 4,
|
|
runSpacing: 4,
|
|
children: [
|
|
BadgeStyles.eventStatus(event.status!),
|
|
],
|
|
),
|
|
const SizedBox(height: 4),
|
|
],
|
|
Text(
|
|
dateFormat.format(event.startDate),
|
|
style: TextStyle(
|
|
color: Colors.grey[700],
|
|
),
|
|
),
|
|
if (event.location != null)
|
|
Text(
|
|
event.location!,
|
|
style: TextStyle(
|
|
color: Colors.grey[600],
|
|
),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
],
|
|
),
|
|
trailing: const Icon(Icons.arrow_forward_ios, size: 16),
|
|
onTap: () {
|
|
// 이벤트 상세 화면으로 이동 (추후 구현)
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|