import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lunchpick/core/utils/category_mapper.dart'; import 'package:lunchpick/domain/entities/restaurant.dart'; import 'package:lunchpick/domain/repositories/restaurant_repository.dart'; import 'package:lunchpick/presentation/providers/di_providers.dart'; import 'package:lunchpick/presentation/providers/location_provider.dart'; import 'package:lunchpick/core/utils/distance_calculator.dart'; import 'package:uuid/uuid.dart'; /// 맛집 목록 Provider final restaurantListProvider = StreamProvider>((ref) { final repository = ref.watch(restaurantRepositoryProvider); return repository.watchRestaurants(); }); /// 거리 정보를 포함한 맛집 목록 Provider (현재 위치 기반) final sortedRestaurantsByDistanceProvider = StreamProvider>((ref) { final restaurantsStream = ref.watch(restaurantListProvider.stream); final positionAsync = ref.watch(currentLocationProvider); final position = positionAsync.maybeWhen( data: (pos) => pos ?? defaultPosition(), orElse: () => defaultPosition(), ); return restaurantsStream.map((restaurants) { final sorted = restaurants.map<({Restaurant restaurant, double? distanceKm})>((r) { final distanceKm = DistanceCalculator.calculateDistance( lat1: position.latitude, lon1: position.longitude, lat2: r.latitude, lon2: r.longitude, ); return (restaurant: r, distanceKm: distanceKm); }).toList()..sort( (a, b) => (a.distanceKm ?? double.infinity).compareTo( b.distanceKm ?? double.infinity, ), ); return sorted; }); }); /// 특정 맛집 Provider final restaurantProvider = FutureProvider.family(( ref, id, ) async { final repository = ref.watch(restaurantRepositoryProvider); return repository.getRestaurantById(id); }); /// 카테고리 목록 Provider (맛집 스트림을 구독해 즉시 갱신) final categoriesProvider = StreamProvider>((ref) { final restaurantsStream = ref.watch(restaurantListProvider.stream); return restaurantsStream.map((restaurants) { final categories = restaurants.map((r) => r.category).toSet().toList() ..sort(); return categories; }); }); /// 맛집 관리 StateNotifier class RestaurantNotifier extends StateNotifier> { final RestaurantRepository _repository; RestaurantNotifier(this._repository) : super(const AsyncValue.data(null)); /// 맛집 추가 Future addRestaurant({ required String name, required String category, required String subCategory, String? description, String? phoneNumber, required String roadAddress, required String jibunAddress, required double latitude, required double longitude, required DataSource source, }) async { state = const AsyncValue.loading(); try { final restaurant = Restaurant( id: const Uuid().v4(), name: name, category: category, subCategory: subCategory, description: description, phoneNumber: phoneNumber, roadAddress: roadAddress, jibunAddress: jibunAddress, latitude: latitude, longitude: longitude, source: source, createdAt: DateTime.now(), updatedAt: DateTime.now(), ); await _repository.addRestaurant(restaurant); state = const AsyncValue.data(null); } catch (e, stack) { state = AsyncValue.error(e, stack); } } /// 맛집 수정 Future updateRestaurant(Restaurant restaurant) async { state = const AsyncValue.loading(); try { final nextSource = restaurant.source == DataSource.PRESET ? DataSource.USER_INPUT : restaurant.source; await _repository.updateRestaurant( restaurant.copyWith(source: nextSource, updatedAt: DateTime.now()), ); state = const AsyncValue.data(null); } catch (e, stack) { state = AsyncValue.error(e, stack); } } /// 맛집 삭제 Future deleteRestaurant(String id) async { state = const AsyncValue.loading(); try { await _repository.deleteRestaurant(id); state = const AsyncValue.data(null); } catch (e, stack) { state = AsyncValue.error(e, stack); } } /// 마지막 방문일 업데이트 Future updateLastVisitDate( String restaurantId, DateTime visitDate, ) async { try { await _repository.updateLastVisitDate(restaurantId, visitDate); } catch (e, stack) { state = AsyncValue.error(e, stack); } } /// 네이버 지도 URL로부터 맛집 추가 Future addRestaurantFromUrl(String url) async { state = const AsyncValue.loading(); try { final restaurant = await _repository.addRestaurantFromUrl(url); state = const AsyncValue.data(null); return restaurant; } catch (e, stack) { state = AsyncValue.error(e, stack); rethrow; } } /// 미리 생성된 Restaurant 객체를 직접 추가 Future addRestaurantDirect(Restaurant restaurant) async { state = const AsyncValue.loading(); try { await _repository.addRestaurant(restaurant); state = const AsyncValue.data(null); } catch (e, stack) { state = AsyncValue.error(e, stack); rethrow; } } } /// RestaurantNotifier Provider final restaurantNotifierProvider = StateNotifierProvider>((ref) { final repository = ref.watch(restaurantRepositoryProvider); return RestaurantNotifier(repository); }); /// 거리 내 맛집 Provider final restaurantsWithinDistanceProvider = FutureProvider.family< List, ({double latitude, double longitude, double maxDistance}) >((ref, params) async { final repository = ref.watch(restaurantRepositoryProvider); return repository.getRestaurantsWithinDistance( userLatitude: params.latitude, userLongitude: params.longitude, maxDistanceInMeters: params.maxDistance, ); }); /// n일 이내 방문하지 않은 맛집 Provider final restaurantsNotVisitedInDaysProvider = FutureProvider.family, int>((ref, days) async { final repository = ref.watch(restaurantRepositoryProvider); return repository.getRestaurantsNotVisitedInDays(days); }); /// 검색어로 맛집 검색 Provider final searchRestaurantsProvider = FutureProvider.family, String>((ref, query) async { final repository = ref.watch(restaurantRepositoryProvider); return repository.searchRestaurants(query); }); /// 카테고리별 맛집 Provider final restaurantsByCategoryProvider = FutureProvider.family, String>((ref, category) async { final repository = ref.watch(restaurantRepositoryProvider); return repository.getRestaurantsByCategory(category); }); /// 검색 쿼리 상태 Provider final searchQueryProvider = StateProvider((ref) => ''); /// 선택된 카테고리 상태 Provider final selectedCategoryProvider = StateProvider((ref) => null); /// 필터링된 맛집 목록 Provider (검색 + 카테고리) final filteredRestaurantsProvider = StreamProvider>(( ref, ) async* { final searchQuery = ref.watch(searchQueryProvider); final selectedCategory = ref.watch(selectedCategoryProvider); final restaurantsStream = ref.watch(restaurantListProvider.stream); await for (final restaurants in restaurantsStream) { var filtered = restaurants; // 검색 필터 적용 if (searchQuery.isNotEmpty) { final lowercaseQuery = searchQuery.toLowerCase(); filtered = filtered.where((restaurant) { return restaurant.name.toLowerCase().contains(lowercaseQuery) || (restaurant.description?.toLowerCase().contains(lowercaseQuery) ?? false) || restaurant.category.toLowerCase().contains(lowercaseQuery); }).toList(); } // 카테고리 필터 적용 if (selectedCategory != null) { filtered = filtered.where((restaurant) { // 정확한 일치 또는 부분 일치 확인 // restaurant.category가 "음식점>한식>백반/한정식" 형태일 때 // selectedCategory가 "백반/한정식"이면 매칭 return restaurant.category == selectedCategory || restaurant.category.contains(selectedCategory) || CategoryMapper.normalizeNaverCategory( restaurant.category, restaurant.subCategory, ) == selectedCategory || CategoryMapper.getDisplayName(restaurant.category) == selectedCategory; }).toList(); } yield filtered; } });