import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lunchpick/domain/entities/recommendation_record.dart'; import 'package:lunchpick/domain/entities/restaurant.dart'; import 'package:lunchpick/domain/repositories/recommendation_repository.dart'; import 'package:lunchpick/domain/usecases/recommendation_engine.dart'; import 'package:lunchpick/presentation/providers/di_providers.dart'; import 'package:lunchpick/presentation/providers/restaurant_provider.dart'; import 'package:lunchpick/presentation/providers/settings_provider.dart' hide currentLocationProvider, locationPermissionProvider; import 'package:lunchpick/presentation/providers/weather_provider.dart'; import 'package:lunchpick/presentation/providers/location_provider.dart'; import 'package:lunchpick/presentation/providers/visit_provider.dart'; import 'package:uuid/uuid.dart'; /// 추천 기록 목록 Provider final recommendationRecordsProvider = StreamProvider>((ref) { final repository = ref.watch(recommendationRepositoryProvider); return repository.watchRecommendationRecords(); }); /// 오늘의 추천 횟수 Provider final todayRecommendationCountProvider = FutureProvider((ref) async { final repository = ref.watch(recommendationRepositoryProvider); return repository.getTodayRecommendationCount(); }); /// 추천 설정 모델 class RecommendationSettings { final int daysToExclude; final int maxDistanceRainy; final int maxDistanceNormal; final List selectedCategories; RecommendationSettings({ required this.daysToExclude, required this.maxDistanceRainy, required this.maxDistanceNormal, required this.selectedCategories, }); } /// 추천 관리 StateNotifier class RecommendationNotifier extends StateNotifier> { final RecommendationRepository _repository; final Ref _ref; final RecommendationEngine _recommendationEngine = RecommendationEngine(); RecommendationNotifier(this._repository, this._ref) : super(const AsyncValue.data(null)); /// 랜덤 추천 실행 Future getRandomRecommendation({ required double maxDistance, required List selectedCategories, }) async { state = const AsyncValue.loading(); try { // 현재 위치 가져오기 final location = await _ref.read(currentLocationProvider.future); if (location == null) { throw Exception('위치 정보를 가져올 수 없습니다'); } // 날씨 정보 가져오기 final weather = await _ref.read(weatherProvider.future); // 사용자 설정 가져오기 final userSettings = await _ref.read(userSettingsProvider.future); // 모든 식당 가져오기 final allRestaurants = await _ref.read(restaurantListProvider.future); // 방문 기록 가져오기 final allVisitRecords = await _ref.read(visitRecordsProvider.future); // 추천 설정 구성 final config = RecommendationConfig( userLatitude: location.latitude, userLongitude: location.longitude, maxDistance: maxDistance, selectedCategories: selectedCategories, userSettings: userSettings, weather: weather, ); // 추천 엔진 사용 final selectedRestaurant = await _recommendationEngine.generateRecommendation( allRestaurants: allRestaurants, recentVisits: allVisitRecords, config: config, ); if (selectedRestaurant == null) { state = const AsyncValue.data(null); return; } // 추천 기록 저장 await _saveRecommendationRecord(selectedRestaurant); state = AsyncValue.data(selectedRestaurant); } catch (e, stack) { state = AsyncValue.error(e, stack); } } /// 추천 기록 저장 Future _saveRecommendationRecord(Restaurant restaurant) async { final record = RecommendationRecord( id: const Uuid().v4(), restaurantId: restaurant.id, recommendationDate: DateTime.now(), visited: false, createdAt: DateTime.now(), ); await _repository.addRecommendationRecord(record); } /// 추천 후 방문 확인 Future confirmVisit(String recommendationId) async { try { await _repository.markAsVisited(recommendationId); // 방문 기록도 생성 final recommendations = await _ref.read(recommendationRecordsProvider.future); final recommendation = recommendations.firstWhere((r) => r.id == recommendationId); final visitNotifier = _ref.read(visitNotifierProvider.notifier); await visitNotifier.createVisitFromRecommendation( restaurantId: recommendation.restaurantId, recommendationTime: recommendation.recommendationDate, ); } catch (e, stack) { state = AsyncValue.error(e, stack); } } /// 추천 기록 삭제 Future deleteRecommendation(String id) async { try { await _repository.deleteRecommendationRecord(id); } catch (e, stack) { state = AsyncValue.error(e, stack); } } } /// RecommendationNotifier Provider final recommendationNotifierProvider = StateNotifierProvider>((ref) { final repository = ref.watch(recommendationRepositoryProvider); return RecommendationNotifier(repository, ref); }); /// 월별 추천 통계 Provider final monthlyRecommendationStatsProvider = FutureProvider.family, ({int year, int month})>((ref, params) async { final repository = ref.watch(recommendationRepositoryProvider); return repository.getMonthlyRecommendationStats(params.year, params.month); }); /// 추천 상태 관리 (다시 추천 기능 포함) class RecommendationState { final Restaurant? currentRecommendation; final List excludedRestaurants; final bool isLoading; final String? error; const RecommendationState({ this.currentRecommendation, this.excludedRestaurants = const [], this.isLoading = false, this.error, }); RecommendationState copyWith({ Restaurant? currentRecommendation, List? excludedRestaurants, bool? isLoading, String? error, }) { return RecommendationState( currentRecommendation: currentRecommendation ?? this.currentRecommendation, excludedRestaurants: excludedRestaurants ?? this.excludedRestaurants, isLoading: isLoading ?? this.isLoading, error: error, ); } } /// 향상된 추천 StateNotifier (다시 추천 기능 포함) class EnhancedRecommendationNotifier extends StateNotifier { final Ref _ref; final RecommendationEngine _recommendationEngine = RecommendationEngine(); EnhancedRecommendationNotifier(this._ref) : super(const RecommendationState()); /// 다시 추천 (현재 추천 제외) Future rerollRecommendation() async { if (state.currentRecommendation == null) return; // 현재 추천을 제외 목록에 추가 final excluded = [...state.excludedRestaurants, state.currentRecommendation!]; state = state.copyWith(excludedRestaurants: excluded); // 다시 추천 생성 (제외 목록 적용) await generateRecommendation(excludedRestaurants: excluded); } /// 추천 생성 (새로운 추천 엔진 활용) Future generateRecommendation({List? excludedRestaurants}) async { state = state.copyWith(isLoading: true); try { // 현재 위치 가져오기 final location = await _ref.read(currentLocationProvider.future); if (location == null) { state = state.copyWith(error: '위치 정보를 가져올 수 없습니다', isLoading: false); return; } // 필요한 데이터 가져오기 final weather = await _ref.read(weatherProvider.future); final userSettings = await _ref.read(userSettingsProvider.future); final allRestaurants = await _ref.read(restaurantListProvider.future); final allVisitRecords = await _ref.read(visitRecordsProvider.future); final maxDistanceNormal = await _ref.read(maxDistanceNormalProvider.future); final selectedCategory = _ref.read(selectedCategoryProvider); final categories = selectedCategory != null ? [selectedCategory] : []; // 제외 리스트 포함한 식당 필터링 final availableRestaurants = excludedRestaurants != null ? allRestaurants.where((r) => !excludedRestaurants.any((ex) => ex.id == r.id)).toList() : allRestaurants; // 추천 설정 구성 final config = RecommendationConfig( userLatitude: location.latitude, userLongitude: location.longitude, maxDistance: maxDistanceNormal.toDouble(), selectedCategories: categories, userSettings: userSettings, weather: weather, ); // 추천 엔진 사용 final selectedRestaurant = await _recommendationEngine.generateRecommendation( allRestaurants: availableRestaurants, recentVisits: allVisitRecords, config: config, ); if (selectedRestaurant != null) { // 추천 기록 저장 final record = RecommendationRecord( id: const Uuid().v4(), restaurantId: selectedRestaurant.id, recommendationDate: DateTime.now(), visited: false, createdAt: DateTime.now(), ); final repository = _ref.read(recommendationRepositoryProvider); await repository.addRecommendationRecord(record); state = state.copyWith( currentRecommendation: selectedRestaurant, isLoading: false, ); } else { state = state.copyWith( error: '조건에 맞는 맛집이 없습니다', isLoading: false, ); } } catch (e) { state = state.copyWith( error: e.toString(), isLoading: false, ); } } /// 추천 초기화 void resetRecommendation() { state = const RecommendationState(); } } /// 향상된 추천 Provider final enhancedRecommendationProvider = StateNotifierProvider((ref) { return EnhancedRecommendationNotifier(ref); }); /// 추천 가능한 맛집 수 Provider final recommendableRestaurantsCountProvider = FutureProvider((ref) async { final daysToExclude = await ref.watch(daysToExcludeProvider.future); final recentlyVisited = await ref.watch( restaurantsNotVisitedInDaysProvider(daysToExclude).future ); return recentlyVisited.length; }); /// 카테고리별 추천 통계 Provider final recommendationStatsByCategoryProvider = FutureProvider>((ref) async { final records = await ref.watch(recommendationRecordsProvider.future); final stats = {}; for (final record in records) { final restaurant = await ref.watch(restaurantProvider(record.restaurantId).future); if (restaurant != null) { stats[restaurant.category] = (stats[restaurant.category] ?? 0) + 1; } } return stats; }); /// 추천 성공률 Provider final recommendationSuccessRateProvider = FutureProvider((ref) async { final records = await ref.watch(recommendationRecordsProvider.future); if (records.isEmpty) return 0.0; final visitedCount = records.where((r) => r.visited).length; return (visitedCount / records.length) * 100; }); /// 가장 많이 추천된 맛집 Top 5 Provider final topRecommendedRestaurantsProvider = FutureProvider>((ref) async { final records = await ref.watch(recommendationRecordsProvider.future); final counts = {}; for (final record in records) { counts[record.restaurantId] = (counts[record.restaurantId] ?? 0) + 1; } final sorted = counts.entries.toList() ..sort((a, b) => b.value.compareTo(a.value)); return sorted.take(5).map((e) => (restaurantId: e.key, count: e.value)).toList(); });