import 'dart:math'; import 'package:lunchpick/domain/entities/restaurant.dart'; import 'package:lunchpick/domain/entities/user_settings.dart'; import 'package:lunchpick/domain/entities/visit_record.dart'; import 'package:lunchpick/domain/entities/weather_info.dart'; import 'package:lunchpick/core/utils/distance_calculator.dart'; /// 추천 엔진 설정 class RecommendationConfig { final double userLatitude; final double userLongitude; final double maxDistance; final List selectedCategories; final UserSettings userSettings; final WeatherInfo? weather; final DateTime currentTime; RecommendationConfig({ required this.userLatitude, required this.userLongitude, required this.maxDistance, required this.selectedCategories, required this.userSettings, this.weather, DateTime? currentTime, }) : currentTime = currentTime ?? DateTime.now(); } /// 추천 엔진 UseCase class RecommendationEngine { final Random _random = Random(); /// 추천 생성 Future generateRecommendation({ required List allRestaurants, required List recentVisits, required RecommendationConfig config, }) async { // 1단계: 거리 필터링 final restaurantsInRange = _filterByDistance(allRestaurants, config); if (restaurantsInRange.isEmpty) return null; // 2단계: 재방문 방지 필터링 final eligibleRestaurants = _filterByRevisitPrevention( restaurantsInRange, recentVisits, config.userSettings.revisitPreventionDays, ); if (eligibleRestaurants.isEmpty) return null; // 3단계: 카테고리 필터링 final filteredByCategory = _filterByCategory( eligibleRestaurants, config.selectedCategories, ); if (filteredByCategory.isEmpty) return null; // 4단계: 가중치 계산 및 선택 return _selectWithWeights(filteredByCategory, config); } /// 거리 기반 필터링 List _filterByDistance( List restaurants, RecommendationConfig config, ) { // 날씨에 따른 최대 거리 조정 double effectiveMaxDistance = config.maxDistance; if (config.weather != null && config.weather!.current.isRainy) { // 비가 올 때는 거리를 70%로 줄임 effectiveMaxDistance *= 0.7; } return restaurants.where((restaurant) { final distance = DistanceCalculator.calculateDistance( lat1: config.userLatitude, lon1: config.userLongitude, lat2: restaurant.latitude, lon2: restaurant.longitude, ); return distance <= effectiveMaxDistance; }).toList(); } /// 재방문 방지 필터링 List _filterByRevisitPrevention( List restaurants, List recentVisits, int preventionDays, ) { final now = DateTime.now(); final cutoffDate = now.subtract(Duration(days: preventionDays)); // 최근 n일 내 방문한 식당 ID 수집 final recentlyVisitedIds = recentVisits .where((visit) => visit.visitDate.isAfter(cutoffDate)) .map((visit) => visit.restaurantId) .toSet(); // 최근 방문하지 않은 식당만 필터링 final filtered = restaurants.where((restaurant) { return !recentlyVisitedIds.contains(restaurant.id); }).toList(); if (filtered.isNotEmpty) return filtered; // 모든 식당이 제외되면 가장 오래전에 방문한 식당을 반환 final lastVisitByRestaurant = {}; for (final visit in recentVisits) { final current = lastVisitByRestaurant[visit.restaurantId]; if (current == null || visit.visitDate.isAfter(current)) { lastVisitByRestaurant[visit.restaurantId] = visit.visitDate; } } Restaurant? oldestRestaurant; DateTime? oldestVisitDate; for (final restaurant in restaurants) { final lastVisit = lastVisitByRestaurant[restaurant.id]; if (lastVisit == null) continue; if (oldestVisitDate == null || lastVisit.isBefore(oldestVisitDate)) { oldestVisitDate = lastVisit; oldestRestaurant = restaurant; } } return oldestRestaurant != null ? [oldestRestaurant] : restaurants; } /// 카테고리 필터링 List _filterByCategory( List restaurants, List selectedCategories, ) { if (selectedCategories.isEmpty) { return restaurants; } return restaurants.where((restaurant) { return selectedCategories.contains(restaurant.category); }).toList(); } /// 가중치 기반 선택 Restaurant? _selectWithWeights( List restaurants, RecommendationConfig config, ) { if (restaurants.isEmpty) return null; // 가중치 미적용: 거리/방문 필터를 통과한 식당 중 균등 무작위 선택 return restaurants[_random.nextInt(restaurants.length)]; } }