Files
lunchpick/lib/domain/usecases/recommendation_engine.dart

155 lines
4.8 KiB
Dart

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<String> 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<Restaurant?> generateRecommendation({
required List<Restaurant> allRestaurants,
required List<VisitRecord> 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<Restaurant> _filterByDistance(
List<Restaurant> 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<Restaurant> _filterByRevisitPrevention(
List<Restaurant> restaurants,
List<VisitRecord> 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 = <String, DateTime>{};
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<Restaurant> _filterByCategory(
List<Restaurant> restaurants,
List<String> selectedCategories,
) {
if (selectedCategories.isEmpty) {
return restaurants;
}
return restaurants.where((restaurant) {
return selectedCategories.contains(restaurant.category);
}).toList();
}
/// 가중치 기반 선택
Restaurant? _selectWithWeights(
List<Restaurant> restaurants,
RecommendationConfig config,
) {
if (restaurants.isEmpty) return null;
// 가중치 미적용: 거리/방문 필터를 통과한 식당 중 균등 무작위 선택
return restaurants[_random.nextInt(restaurants.length)];
}
}