import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:geolocator/geolocator.dart'; import '../../../core/constants/app_colors.dart'; import '../../../core/constants/app_typography.dart'; import '../../../domain/entities/restaurant.dart'; import '../../../domain/entities/weather_info.dart'; import '../../providers/ad_provider.dart'; import '../../providers/location_provider.dart'; import '../../providers/notification_provider.dart'; import '../../providers/recommendation_provider.dart'; import '../../providers/restaurant_provider.dart'; import '../../providers/settings_provider.dart' show notificationDelayMinutesProvider, notificationEnabledProvider; import '../../providers/visit_provider.dart'; import '../../providers/weather_provider.dart'; import 'widgets/recommendation_result_dialog.dart'; class RandomSelectionScreen extends ConsumerStatefulWidget { const RandomSelectionScreen({super.key}); @override ConsumerState createState() => _RandomSelectionScreenState(); } class _RandomSelectionScreenState extends ConsumerState { double _distanceValue = 500; final List _selectedCategories = []; bool _isProcessingRecommendation = false; @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('오늘 뭐 먹Z?'), backgroundColor: isDark ? AppColors.darkPrimary : AppColors.lightPrimary, foregroundColor: Colors.white, elevation: 0, ), body: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // 맛집 리스트 현황 카드 Card( color: isDark ? AppColors.darkSurface : AppColors.lightSurface, elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), child: Padding( padding: const EdgeInsets.all(20), child: Column( children: [ const Icon( Icons.restaurant, size: 48, color: AppColors.lightPrimary, ), const SizedBox(height: 12), Consumer( builder: (context, ref, child) { final restaurantsAsync = ref.watch( restaurantListProvider, ); return restaurantsAsync.when( data: (restaurants) => Text( '${restaurants.length}개', style: AppTypography.heading1( isDark, ).copyWith(color: AppColors.lightPrimary), ), loading: () => const CircularProgressIndicator( color: AppColors.lightPrimary, ), error: (_, __) => Text( '0개', style: AppTypography.heading1( isDark, ).copyWith(color: AppColors.lightPrimary), ), ); }, ), Text('등록된 맛집', style: AppTypography.body2(isDark)), ], ), ), ), const SizedBox(height: 16), // 날씨 정보 카드 Card( color: isDark ? AppColors.darkSurface : AppColors.lightSurface, elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), child: Padding( padding: const EdgeInsets.all(16), child: Consumer( builder: (context, ref, child) { final weatherAsync = ref.watch(weatherProvider); return weatherAsync.when( data: (weather) => Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _buildWeatherData('지금', weather.current, isDark), Container( width: 1, height: 50, color: isDark ? AppColors.darkDivider : AppColors.lightDivider, ), _buildWeatherData('1시간 후', weather.nextHour, isDark), ], ), loading: () => const Center( child: CircularProgressIndicator( color: AppColors.lightPrimary, ), ), error: (_, __) => Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _buildWeatherInfo( '지금', Icons.wb_sunny, '맑음', 20, isDark, ), Container( width: 1, height: 50, color: isDark ? AppColors.darkDivider : AppColors.lightDivider, ), _buildWeatherInfo( '1시간 후', Icons.wb_sunny, '맑음', 22, isDark, ), ], ), ); }, ), ), ), const SizedBox(height: 16), // 거리 설정 카드 Card( color: isDark ? AppColors.darkSurface : AppColors.lightSurface, elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('최대 거리', style: AppTypography.heading2(isDark)), const SizedBox(height: 12), Row( children: [ Expanded( child: SliderTheme( data: SliderTheme.of(context).copyWith( activeTrackColor: AppColors.lightPrimary, inactiveTrackColor: AppColors.lightPrimary .withValues(alpha: 0.3), thumbColor: AppColors.lightPrimary, trackHeight: 4, ), child: Slider( value: _distanceValue, min: 100, max: 2000, divisions: 19, onChanged: (value) { setState(() => _distanceValue = value); }, ), ), ), const SizedBox(width: 12), Text( '${_distanceValue.toInt()}m', style: AppTypography.body1( isDark, ).copyWith(fontWeight: FontWeight.bold), ), ], ), const SizedBox(height: 8), Consumer( builder: (context, ref, child) { final locationAsync = ref.watch( currentLocationProvider, ); final restaurantsAsync = ref.watch( restaurantListProvider, ); if (locationAsync.hasValue && restaurantsAsync.hasValue) { final location = locationAsync.value; final restaurants = restaurantsAsync.value; if (location != null && restaurants != null) { final count = _getRestaurantCountInRange( restaurants, location, _distanceValue, ); return Text( '$count개 맛집 포함', style: AppTypography.caption(isDark), ); } } return Text( '위치 정보를 가져오는 중...', style: AppTypography.caption(isDark), ); }, ), ], ), ), ), const SizedBox(height: 16), // 카테고리 선택 카드 Card( color: isDark ? AppColors.darkSurface : AppColors.lightSurface, elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('카테고리', style: AppTypography.heading2(isDark)), const SizedBox(height: 12), Consumer( builder: (context, ref, child) { final categoriesAsync = ref.watch(categoriesProvider); return categoriesAsync.when( data: (categories) => Wrap( spacing: 8, runSpacing: 8, children: categories.isEmpty ? [const Text('카테고리 없음')] : categories .map( (category) => _buildCategoryChip( category, isDark, ), ) .toList(), ), loading: () => const CircularProgressIndicator(), error: (_, __) => const Text('카테고리를 불러올 수 없습니다'), ); }, ), ], ), ), ), const SizedBox(height: 24), // 추천받기 버튼 ElevatedButton( onPressed: !_isProcessingRecommendation && _canRecommend() ? () => _startRecommendation() : null, style: ElevatedButton.styleFrom( backgroundColor: AppColors.lightPrimary, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 20), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), elevation: 3, ), child: _isProcessingRecommendation ? const SizedBox( height: 24, width: 24, child: CircularProgressIndicator( strokeWidth: 2.5, color: Colors.white, ), ) : const Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.play_arrow, size: 28), SizedBox(width: 8), Text( '광고보고 추천받기', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), ], ), ), ], ), ), ); } Widget _buildWeatherData(String label, WeatherData weatherData, bool isDark) { return Column( children: [ Text(label, style: AppTypography.caption(isDark)), const SizedBox(height: 8), Icon( weatherData.isRainy ? Icons.umbrella : Icons.wb_sunny, color: weatherData.isRainy ? Colors.blue : Colors.orange, size: 32, ), const SizedBox(height: 4), Text( '${weatherData.temperature}°C', style: AppTypography.body1( isDark, ).copyWith(fontWeight: FontWeight.bold), ), Text(weatherData.description, style: AppTypography.caption(isDark)), ], ); } Widget _buildWeatherInfo( String label, IconData icon, String description, int temperature, bool isDark, ) { return Column( children: [ Text(label, style: AppTypography.caption(isDark)), const SizedBox(height: 8), Icon(icon, color: Colors.orange, size: 32), const SizedBox(height: 4), Text( '$temperature°C', style: AppTypography.body1( isDark, ).copyWith(fontWeight: FontWeight.bold), ), Text(description, style: AppTypography.caption(isDark)), ], ); } Widget _buildCategoryChip(String category, bool isDark) { final isSelected = _selectedCategories.contains(category); return FilterChip( label: Text(category), selected: isSelected, onSelected: (selected) { setState(() { if (selected) { _selectedCategories.add(category); } else { _selectedCategories.remove(category); } }); }, backgroundColor: isDark ? AppColors.darkSurface : AppColors.lightBackground, selectedColor: AppColors.lightPrimary.withValues(alpha: 0.2), checkmarkColor: AppColors.lightPrimary, labelStyle: TextStyle( color: isSelected ? AppColors.lightPrimary : (isDark ? AppColors.darkTextPrimary : AppColors.lightTextPrimary), ), side: BorderSide( color: isSelected ? AppColors.lightPrimary : (isDark ? AppColors.darkDivider : AppColors.lightDivider), ), ); } int _getRestaurantCountInRange( List restaurants, Position location, double maxDistance, ) { return restaurants.where((restaurant) { final distance = Geolocator.distanceBetween( location.latitude, location.longitude, restaurant.latitude, restaurant.longitude, ); return distance <= maxDistance; }).length; } bool _canRecommend() { final locationAsync = ref.read(currentLocationProvider); final restaurantsAsync = ref.read(restaurantListProvider); if (!locationAsync.hasValue || !restaurantsAsync.hasValue) { return false; } final location = locationAsync.value; final restaurants = restaurantsAsync.value; if (location == null || restaurants == null || restaurants.isEmpty) { return false; } final count = _getRestaurantCountInRange( restaurants, location, _distanceValue, ); return count > 0; } Future _startRecommendation({bool skipAd = false}) async { if (_isProcessingRecommendation) return; setState(() { _isProcessingRecommendation = true; }); try { final candidate = await _generateRecommendationCandidate(); if (candidate == null) { return; } if (!skipAd) { final adService = ref.read(adServiceProvider); // Ad dialog 자체가 비동기 동작을 포함하므로 사용 후 mounted 체크를 수행한다. // ignore: use_build_context_synchronously final adWatched = await adService.showInterstitialAd(context); if (!mounted) return; if (!adWatched) { _showSnack( '광고를 끝까지 시청해야 추천을 받을 수 있어요.', backgroundColor: AppColors.lightError, ); return; } } if (!mounted) return; _showRecommendationDialog(candidate); } catch (_) { _showSnack( '추천을 준비하는 중 문제가 발생했습니다.', backgroundColor: AppColors.lightError, ); } finally { if (mounted) { setState(() { _isProcessingRecommendation = false; }); } } } Future _generateRecommendationCandidate() async { final notifier = ref.read(recommendationNotifierProvider.notifier); await notifier.getRandomRecommendation( maxDistance: _distanceValue, selectedCategories: _selectedCategories, ); final result = ref.read(recommendationNotifierProvider); if (result.hasError) { final message = result.error?.toString() ?? '알 수 없는 오류'; _showSnack( '추천 중 오류가 발생했습니다: $message', backgroundColor: AppColors.lightError, ); return null; } final restaurant = result.asData?.value; if (restaurant == null) { _showSnack('조건에 맞는 식당이 존재하지 않습니다', backgroundColor: AppColors.lightError); } return restaurant; } void _showRecommendationDialog(Restaurant restaurant) { showDialog( context: context, barrierDismissible: false, builder: (dialogContext) => RecommendationResultDialog( restaurant: restaurant, onReroll: () async { Navigator.pop(dialogContext); await _startRecommendation(skipAd: true); }, onClose: () async { Navigator.pop(dialogContext); await _handleRecommendationAccepted(restaurant); }, ), ); } Future _handleRecommendationAccepted(Restaurant restaurant) async { final recommendationTime = DateTime.now(); try { final notificationEnabled = await ref.read( notificationEnabledProvider.future, ); if (notificationEnabled) { final delayMinutes = await ref.read( notificationDelayMinutesProvider.future, ); final notificationService = ref.read(notificationServiceProvider); await notificationService.scheduleVisitReminder( restaurantId: restaurant.id, restaurantName: restaurant.name, recommendationTime: recommendationTime, delayMinutes: delayMinutes, ); } await ref .read(visitNotifierProvider.notifier) .createVisitFromRecommendation( restaurantId: restaurant.id, recommendationTime: recommendationTime, ); _showSnack('맛있게 드세요! 🍴'); } catch (_) { _showSnack( '방문 기록 또는 알림 예약에 실패했습니다.', backgroundColor: AppColors.lightError, ); } } void _showSnack( String message, { Color backgroundColor = AppColors.lightPrimary, }) { if (!mounted) return; ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar( SnackBar(content: Text(message), backgroundColor: backgroundColor), ); } }