Line data Source code
1 : import 'package:flutter/material.dart';
2 : import 'package:provider/provider.dart';
3 : import 'package:intl/intl.dart';
4 :
5 : import '../../services/auth_service.dart';
6 : import '../../services/club_service.dart';
7 : import '../../services/member_service.dart';
8 : import '../../services/event_service.dart';
9 : import '../../models/club_model.dart';
10 : import 'club_settings_screen.dart';
11 :
12 : class DashboardScreen extends StatefulWidget {
13 24 : const DashboardScreen({super.key});
14 :
15 0 : @override
16 0 : State<DashboardScreen> createState() => _DashboardScreenState();
17 : }
18 :
19 : class _DashboardScreenState extends State<DashboardScreen> {
20 : bool _isLoading = false;
21 : bool _isInit = false;
22 :
23 0 : @override
24 : void didChangeDependencies() {
25 0 : super.didChangeDependencies();
26 0 : if (!_isInit) {
27 0 : _loadDashboardData();
28 0 : _isInit = true;
29 : }
30 : }
31 :
32 0 : Future<void> _loadDashboardData() async {
33 0 : setState(() {
34 0 : _isLoading = true;
35 : });
36 :
37 : try {
38 0 : final authService = Provider.of<AuthService>(context, listen: false);
39 0 : final clubService = Provider.of<ClubService>(context, listen: false);
40 :
41 : // 클럽 정보 로드
42 0 : if (clubService.currentClub == null) {
43 0 : await clubService.initialize(authService.token!);
44 : }
45 :
46 : // 회원 및 이벤트 서비스 초기화
47 0 : if (clubService.currentClub != null) {
48 0 : final memberService = Provider.of<MemberService>(context, listen: false);
49 0 : final eventService = Provider.of<EventService>(context, listen: false);
50 :
51 0 : memberService.initialize(authService.token!);
52 0 : eventService.initialize(authService.token!);
53 :
54 : // 회원 및 이벤트 데이터 로드
55 0 : await Future.wait([
56 0 : memberService.fetchClubMembers(),
57 0 : eventService.fetchClubEvents(),
58 : ]);
59 : }
60 : } catch (e) {
61 : // 오류 처리
62 0 : ScaffoldMessenger.of(context).showSnackBar(
63 0 : SnackBar(content: Text('데이터 로드 중 오류가 발생했습니다: $e')),
64 : );
65 : } finally {
66 0 : setState(() {
67 0 : _isLoading = false;
68 : });
69 : }
70 : }
71 :
72 0 : @override
73 : Widget build(BuildContext context) {
74 0 : return Scaffold(
75 0 : body: _isLoading
76 : ? const Center(child: CircularProgressIndicator())
77 0 : : RefreshIndicator(
78 0 : onRefresh: _loadDashboardData,
79 0 : child: Consumer3<ClubService, MemberService, EventService>(
80 0 : builder: (context, clubService, memberService, eventService, _) {
81 0 : final club = clubService.currentClub;
82 :
83 : if (club == null) {
84 : return const Center(
85 : child: Text('클럽 정보를 불러올 수 없습니다'),
86 : );
87 : }
88 :
89 0 : return SingleChildScrollView(
90 : physics: const AlwaysScrollableScrollPhysics(),
91 : padding: const EdgeInsets.all(16.0),
92 0 : child: Column(
93 : crossAxisAlignment: CrossAxisAlignment.start,
94 0 : children: [
95 : // 클럽 정보 카드
96 0 : _buildClubInfoCard(club),
97 : const SizedBox(height: 16),
98 :
99 : // 통계 카드들
100 0 : _buildStatisticsCards(
101 0 : memberCount: memberService.members.length,
102 0 : eventCount: eventService.events.length,
103 : ),
104 : const SizedBox(height: 16),
105 :
106 : // 최근 이벤트 목록
107 0 : _buildRecentEventsList(eventService),
108 : ],
109 : ),
110 : );
111 : },
112 : ),
113 : ),
114 : );
115 : }
116 :
117 0 : Widget _buildClubInfoCard(Club club) {
118 0 : return Card(
119 : elevation: 2,
120 0 : shape: RoundedRectangleBorder(
121 0 : borderRadius: BorderRadius.circular(12),
122 : ),
123 0 : child: Padding(
124 : padding: const EdgeInsets.all(16.0),
125 0 : child: Column(
126 : crossAxisAlignment: CrossAxisAlignment.start,
127 0 : children: [
128 0 : Row(
129 : mainAxisAlignment: MainAxisAlignment.spaceBetween,
130 0 : children: [
131 0 : Expanded(
132 0 : child: Row(
133 0 : children: [
134 0 : if (club.logo != null)
135 0 : ClipRRect(
136 0 : borderRadius: BorderRadius.circular(8),
137 0 : child: Image.network(
138 0 : club.logo!,
139 : width: 60,
140 : height: 60,
141 : fit: BoxFit.cover,
142 0 : errorBuilder: (context, error, stackTrace) {
143 0 : return Container(
144 : width: 60,
145 : height: 60,
146 0 : color: Colors.grey[300],
147 : child: const Icon(Icons.sports_golf),
148 : );
149 : },
150 : ),
151 : )
152 : else
153 0 : Container(
154 : width: 60,
155 : height: 60,
156 0 : decoration: BoxDecoration(
157 0 : color: Colors.blue[100],
158 0 : borderRadius: BorderRadius.circular(8),
159 : ),
160 : child: const Icon(
161 : Icons.sports_golf,
162 : size: 30,
163 : color: Colors.blue,
164 : ),
165 : ),
166 0 : const SizedBox(width: 16),
167 0 : Expanded(
168 0 : child: Column(
169 : crossAxisAlignment: CrossAxisAlignment.start,
170 0 : children: [
171 0 : Text(
172 0 : club.name,
173 : style: const TextStyle(
174 : fontSize: 20,
175 : fontWeight: FontWeight.bold,
176 : ),
177 : ),
178 0 : if (club.description != null)
179 0 : Text(
180 0 : club.description!,
181 0 : style: TextStyle(
182 : fontSize: 14,
183 0 : color: Colors.grey[600],
184 : ),
185 : maxLines: 2,
186 : overflow: TextOverflow.ellipsis,
187 : ),
188 : ],
189 : ),
190 : ),
191 : ],
192 : ),
193 : ),
194 0 : IconButton(
195 : icon: const Icon(Icons.settings),
196 : tooltip: '클럽 설정',
197 0 : onPressed: () {
198 0 : Navigator.of(context).pushNamed(ClubSettingsScreen.routeName);
199 : },
200 : ),
201 : ],
202 : ),
203 : const SizedBox(height: 16),
204 0 : if (club.address != null)
205 0 : _buildInfoRow(Icons.location_on, club.address!),
206 0 : if (club.phone != null)
207 0 : _buildInfoRow(Icons.phone, club.phone!),
208 0 : if (club.email != null)
209 0 : _buildInfoRow(Icons.email, club.email!),
210 0 : if (club.website != null)
211 0 : _buildInfoRow(Icons.language, club.website!),
212 : ],
213 : ),
214 : ),
215 : );
216 : }
217 :
218 0 : Widget _buildInfoRow(IconData icon, String text) {
219 0 : return Padding(
220 : padding: const EdgeInsets.only(bottom: 8.0),
221 0 : child: Row(
222 0 : children: [
223 0 : Icon(icon, size: 16, color: Colors.grey[600]),
224 : const SizedBox(width: 8),
225 0 : Expanded(
226 0 : child: Text(
227 : text,
228 0 : style: TextStyle(fontSize: 14, color: Colors.grey[800]),
229 : ),
230 : ),
231 : ],
232 : ),
233 : );
234 : }
235 :
236 0 : Widget _buildStatisticsCards({required int memberCount, required int eventCount}) {
237 0 : return Row(
238 0 : children: [
239 0 : Expanded(
240 0 : child: _buildStatCard(
241 : title: '회원',
242 0 : value: memberCount.toString(),
243 : icon: Icons.people,
244 : color: Colors.blue,
245 : ),
246 : ),
247 : const SizedBox(width: 16),
248 0 : Expanded(
249 0 : child: _buildStatCard(
250 : title: '이벤트',
251 0 : value: eventCount.toString(),
252 : icon: Icons.event,
253 : color: Colors.orange,
254 : ),
255 : ),
256 : ],
257 : );
258 : }
259 :
260 0 : Widget _buildStatCard({
261 : required String title,
262 : required String value,
263 : required IconData icon,
264 : required Color color,
265 : }) {
266 0 : return Card(
267 : elevation: 2,
268 0 : shape: RoundedRectangleBorder(
269 0 : borderRadius: BorderRadius.circular(12),
270 : ),
271 0 : child: Padding(
272 : padding: const EdgeInsets.all(16.0),
273 0 : child: Column(
274 0 : children: [
275 0 : Icon(icon, size: 32, color: color),
276 : const SizedBox(height: 8),
277 0 : Text(
278 : value,
279 0 : style: TextStyle(
280 : fontSize: 24,
281 : fontWeight: FontWeight.bold,
282 : color: color,
283 : ),
284 : ),
285 0 : Text(
286 : title,
287 0 : style: TextStyle(
288 : fontSize: 14,
289 0 : color: Colors.grey[600],
290 : ),
291 : ),
292 : ],
293 : ),
294 : ),
295 : );
296 : }
297 :
298 0 : Widget _buildRecentEventsList(EventService eventService) {
299 0 : final events = eventService.events;
300 :
301 0 : if (events.isEmpty) {
302 : return const Card(
303 : child: Padding(
304 : padding: EdgeInsets.all(16.0),
305 : child: Center(
306 : child: Text('예정된 이벤트가 없습니다'),
307 : ),
308 : ),
309 : );
310 : }
311 :
312 : // 날짜 기준으로 정렬 (가까운 날짜순)
313 0 : final sortedEvents = [...events];
314 0 : sortedEvents.sort((a, b) => a.startDate.compareTo(b.startDate));
315 :
316 : // 앞으로 진행될 이벤트만 필터링 (최대 3개)
317 : final upcomingEvents = sortedEvents
318 0 : .where((event) => event.startDate.isAfter(DateTime.now()))
319 0 : .take(3)
320 0 : .toList();
321 :
322 0 : return Column(
323 : crossAxisAlignment: CrossAxisAlignment.start,
324 0 : children: [
325 0 : Row(
326 : mainAxisAlignment: MainAxisAlignment.spaceBetween,
327 0 : children: [
328 : const Text(
329 : '다가오는 이벤트',
330 : style: TextStyle(
331 : fontSize: 18,
332 : fontWeight: FontWeight.bold,
333 : ),
334 : ),
335 0 : TextButton(
336 0 : onPressed: () {
337 : // 이벤트 목록 화면으로 이동 (추후 구현)
338 : },
339 : child: const Text('모두 보기'),
340 : ),
341 : ],
342 : ),
343 : const SizedBox(height: 8),
344 0 : ...upcomingEvents.map((event) => _buildEventCard(event)),
345 : ],
346 : );
347 : }
348 :
349 0 : Widget _buildEventCard(event) {
350 0 : final dateFormat = DateFormat('yyyy년 MM월 dd일 HH:mm');
351 :
352 0 : return Card(
353 : margin: const EdgeInsets.only(bottom: 12),
354 0 : shape: RoundedRectangleBorder(
355 0 : borderRadius: BorderRadius.circular(12),
356 : ),
357 0 : child: ListTile(
358 : contentPadding: const EdgeInsets.symmetric(
359 : horizontal: 16,
360 : vertical: 8,
361 : ),
362 0 : title: Text(
363 0 : event.title,
364 : style: const TextStyle(
365 : fontWeight: FontWeight.bold,
366 : ),
367 : ),
368 0 : subtitle: Column(
369 : crossAxisAlignment: CrossAxisAlignment.start,
370 0 : children: [
371 : const SizedBox(height: 4),
372 0 : Text(
373 0 : dateFormat.format(event.startDate),
374 0 : style: TextStyle(
375 0 : color: Colors.grey[700],
376 : ),
377 : ),
378 0 : if (event.location != null)
379 0 : Text(
380 0 : event.location!,
381 0 : style: TextStyle(
382 0 : color: Colors.grey[600],
383 : ),
384 : ),
385 : ],
386 : ),
387 : trailing: const Icon(Icons.arrow_forward_ios, size: 16),
388 0 : onTap: () {
389 : // 이벤트 상세 화면으로 이동 (추후 구현)
390 : },
391 : ),
392 : );
393 : }
394 : }
|