import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:table_calendar/table_calendar.dart'; import '../../../core/constants/app_colors.dart'; import '../../../core/constants/app_typography.dart'; import '../../../domain/entities/recommendation_record.dart'; import '../../../domain/entities/visit_record.dart'; import '../../providers/recommendation_provider.dart'; import '../../providers/visit_provider.dart'; import '../../widgets/native_ad_placeholder.dart'; import 'widgets/visit_record_card.dart'; import 'widgets/recommendation_record_card.dart'; import 'widgets/visit_statistics.dart'; class CalendarScreen extends ConsumerStatefulWidget { const CalendarScreen({super.key}); @override ConsumerState createState() => _CalendarScreenState(); } class _CalendarScreenState extends ConsumerState with SingleTickerProviderStateMixin { late DateTime _selectedDay; late DateTime _focusedDay; CalendarFormat _calendarFormat = CalendarFormat.month; late TabController _tabController; Map> _events = {}; @override void initState() { super.initState(); _selectedDay = DateTime.now(); _focusedDay = DateTime.now(); _tabController = TabController(length: 2, vsync: this); } @override void dispose() { _tabController.dispose(); super.dispose(); } List<_CalendarEvent> _getEventsForDay(DateTime day) { final normalizedDay = DateTime(day.year, day.month, day.day); return _events[normalizedDay] ?? []; } @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; return Scaffold( backgroundColor: isDark ? AppColors.darkBackground : AppColors.lightBackground, appBar: AppBar( title: const Text('방문 기록'), backgroundColor: isDark ? AppColors.darkPrimary : AppColors.lightPrimary, foregroundColor: Colors.white, elevation: 0, bottom: TabBar( controller: _tabController, indicatorColor: Colors.white, indicatorWeight: 3, tabs: const [ Tab(text: '캘린더'), Tab(text: '통계'), ], ), ), body: TabBarView( controller: _tabController, children: [ // 캘린더 탭 _buildCalendarTab(isDark), // 통계 탭 VisitStatistics(selectedMonth: _focusedDay), ], ), ); } Widget _buildCalendarTab(bool isDark) { return Consumer( builder: (context, ref, child) { final visitRecordsAsync = ref.watch(visitRecordsProvider); final recommendationRecordsAsync = ref.watch( recommendationRecordsProvider, ); if (visitRecordsAsync.hasValue && recommendationRecordsAsync.hasValue) { final visits = visitRecordsAsync.value ?? []; final recommendations = recommendationRecordsAsync.valueOrNull ?? []; _events = _buildEvents(visits, recommendations); } return LayoutBuilder( builder: (context, constraints) { return SingleChildScrollView( padding: const EdgeInsets.only(bottom: 16), child: ConstrainedBox( constraints: BoxConstraints(minHeight: constraints.maxHeight), child: Column( children: [ Card( margin: const EdgeInsets.all(16), color: isDark ? AppColors.darkSurface : AppColors.lightSurface, elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), child: TableCalendar( firstDay: DateTime.utc(2025, 1, 1), lastDay: DateTime.utc(2030, 12, 31), focusedDay: _focusedDay, calendarFormat: _calendarFormat, selectedDayPredicate: (day) => isSameDay(_selectedDay, day), onDaySelected: (selectedDay, focusedDay) { setState(() { _selectedDay = selectedDay; _focusedDay = focusedDay; }); }, onFormatChanged: (format) { setState(() { _calendarFormat = format; }); }, eventLoader: _getEventsForDay, calendarBuilders: CalendarBuilders( markerBuilder: (context, day, events) { if (events.isEmpty) return null; final calendarEvents = events .cast<_CalendarEvent>(); final confirmedVisits = calendarEvents.where( (e) => e.visitRecord?.isConfirmed == true, ); final recommendedOnly = calendarEvents.where( (e) => e.recommendationRecord != null, ); return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ if (confirmedVisits.isNotEmpty) Container( width: 6, height: 6, margin: const EdgeInsets.symmetric( horizontal: 1, ), decoration: const BoxDecoration( color: AppColors.lightPrimary, shape: BoxShape.circle, ), ), if (recommendedOnly.isNotEmpty) Container( width: 6, height: 6, margin: const EdgeInsets.symmetric( horizontal: 1, ), decoration: const BoxDecoration( color: Colors.orange, shape: BoxShape.circle, ), ), ], ); }, ), calendarStyle: CalendarStyle( outsideDaysVisible: false, selectedDecoration: const BoxDecoration( color: AppColors.lightPrimary, shape: BoxShape.circle, ), todayDecoration: BoxDecoration( color: AppColors.lightPrimary.withOpacity(0.5), shape: BoxShape.circle, ), markersMaxCount: 2, markerDecoration: const BoxDecoration( color: AppColors.lightSecondary, shape: BoxShape.circle, ), weekendTextStyle: const TextStyle( color: AppColors.lightError, ), ), headerStyle: HeaderStyle( formatButtonVisible: true, titleCentered: true, formatButtonShowsNext: false, formatButtonDecoration: BoxDecoration( color: AppColors.lightPrimary.withOpacity(0.1), borderRadius: BorderRadius.circular(12), ), formatButtonTextStyle: const TextStyle( color: AppColors.lightPrimary, ), ), ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ _buildLegend('추천받음', Colors.orange, isDark), const SizedBox(width: 24), _buildLegend('방문완료', Colors.green, isDark), ], ), ), const SizedBox(height: 16), const NativeAdPlaceholder( margin: EdgeInsets.fromLTRB(16, 0, 16, 16), height: 360, ), _buildDayRecords(_selectedDay, isDark), ], ), ), ); }, ); }, ); } Widget _buildLegend(String label, Color color, bool isDark) { return Row( children: [ Container( width: 14, height: 14, decoration: BoxDecoration(color: color, shape: BoxShape.circle), ), const SizedBox(width: 6), Text(label, style: AppTypography.body2(isDark)), ], ); } Widget _buildDayRecords(DateTime day, bool isDark) { final events = _getEventsForDay(day); events.sort((a, b) => b.sortDate.compareTo(a.sortDate)); if (events.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.event_available, size: 48, color: isDark ? AppColors.darkTextSecondary : AppColors.lightTextSecondary, ), const SizedBox(height: 16), Text('이날의 기록이 없습니다', style: AppTypography.body2(isDark)), ], ), ); } return Column( children: [ Padding( padding: const EdgeInsets.all(16), child: Row( children: [ Icon( Icons.calendar_today, size: 20, color: isDark ? AppColors.darkTextSecondary : AppColors.lightTextSecondary, ), const SizedBox(width: 8), Text( '${day.month}월 ${day.day}일 방문 기록', style: AppTypography.body1( isDark, ).copyWith(fontWeight: FontWeight.bold), ), const Spacer(), Text( '${events.length}건', style: AppTypography.body2(isDark).copyWith( color: AppColors.lightPrimary, fontWeight: FontWeight.bold, ), ), ], ), ), ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: events.length, itemBuilder: (context, index) { final event = events[index]; if (event.visitRecord != null) { return VisitRecordCard( visitRecord: event.visitRecord!, onTap: () {}, ); } if (event.recommendationRecord != null) { return RecommendationRecordCard( recommendation: event.recommendationRecord!, onConfirmVisit: () async { await ref .read(recommendationNotifierProvider.notifier) .confirmVisit(event.recommendationRecord!.id); }, onDelete: () async { await ref .read(recommendationNotifierProvider.notifier) .deleteRecommendation(event.recommendationRecord!.id); }, ); } return const SizedBox.shrink(); }, ), ], ); } Map> _buildEvents( List visits, List recommendations, ) { final Map> events = {}; for (final visit in visits) { final day = DateTime( visit.visitDate.year, visit.visitDate.month, visit.visitDate.day, ); events[day] = [ ...(events[day] ?? []), _CalendarEvent(visitRecord: visit), ]; } for (final reco in recommendations.where((r) => !r.visited)) { final day = DateTime( reco.recommendationDate.year, reco.recommendationDate.month, reco.recommendationDate.day, ); events[day] = [ ...(events[day] ?? []), _CalendarEvent(recommendationRecord: reco), ]; } return events; } } class _CalendarEvent { final VisitRecord? visitRecord; final RecommendationRecord? recommendationRecord; _CalendarEvent({this.visitRecord, this.recommendationRecord}); DateTime get sortDate => visitRecord?.visitDate ?? recommendationRecord!.recommendationDate; }