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, List excludedRestaurantIds = const [], bool shouldSaveRecord = true, }) async { state = const AsyncValue.loading(); try { final selectedRestaurant = await _generateCandidate( maxDistance: maxDistance, selectedCategories: selectedCategories, excludedRestaurantIds: excludedRestaurantIds, ); if (selectedRestaurant == null) { state = const AsyncValue.data(null); return null; } if (shouldSaveRecord) { await saveRecommendationRecord(selectedRestaurant); } state = AsyncValue.data(selectedRestaurant); return selectedRestaurant; } catch (e, stack) { state = AsyncValue.error(e, stack); return null; } } Future _generateCandidate({ required double maxDistance, required List selectedCategories, List excludedRestaurantIds = const [], }) async { // 현재 위치 가져오기 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 availableRestaurants = excludedRestaurantIds.isEmpty ? allRestaurants : allRestaurants .where( (restaurant) => !excludedRestaurantIds.contains(restaurant.id), ) .toList(); if (availableRestaurants.isEmpty) { return null; } // 추천 설정 구성 final config = RecommendationConfig( userLatitude: location.latitude, userLongitude: location.longitude, maxDistance: maxDistance, selectedCategories: selectedCategories, userSettings: userSettings, weather: weather, ); // 추천 엔진 사용 return _recommendationEngine.generateRecommendation( allRestaurants: availableRestaurants, recentVisits: allVisitRecords, config: config, ); } /// 추천 기록 저장 Future saveRecommendationRecord( Restaurant restaurant, { DateTime? recommendationTime, bool visited = false, }) async { final now = DateTime.now(); final record = RecommendationRecord( id: const Uuid().v4(), restaurantId: restaurant.id, recommendationDate: recommendationTime ?? now, visited: visited, createdAt: now, ); await _repository.addRecommendationRecord(record); return 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, isConfirmed: true, ); // 방문 기록을 만들었으므로 추천 기록은 숨김 처리 await _repository.deleteRecommendationRecord(recommendationId); } 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(); });