Files
lunchpick/lib/presentation/providers/restaurant_provider.dart
JiWoong Sul 9a61e2f391 fix: 맛집 중복 체크 및 카테고리 필터 로직 개선
1. placeId 기반 중복 체크 제거
   - 이전 대화에서 명확히 한 대로 placeId는 매칭에 사용하지 않음

2. 주소 기반 매칭 개선
   - 주소가 있을 때만 주소 기반 중복 체크 수행

3. 위치 기반 매칭 추가
   - 50m 이내 동일한 이름의 맛집 중복 체크 추가

4. 검색 결과 선택 로직 개선
   - 주소가 없을 때 가장 가까운 거리의 업체 선택

5. 카테고리 필터 버그 수정
   - 카테고리 표시명과 실제 값 불일치 문제 해결
   - 부분 일치 및 정규화된 비교 지원

6. 빈 상태 메시지 개선
   - 필터링 중일 때 적절한 안내 메시지 표시
   - 필터 초기화 버튼 추가

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-30 19:27:04 +09:00

223 lines
7.6 KiB
Dart

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:uuid/uuid.dart';
/// 맛집 목록 Provider
final restaurantListProvider = StreamProvider<List<Restaurant>>((ref) {
final repository = ref.watch(restaurantRepositoryProvider);
return repository.watchRestaurants();
});
/// 특정 맛집 Provider
final restaurantProvider = FutureProvider.family<Restaurant?, String>((ref, id) async {
final repository = ref.watch(restaurantRepositoryProvider);
return repository.getRestaurantById(id);
});
/// 카테고리 목록 Provider
final categoriesProvider = FutureProvider<List<String>>((ref) async {
final repository = ref.watch(restaurantRepositoryProvider);
return repository.getAllCategories();
});
/// 맛집 관리 StateNotifier
class RestaurantNotifier extends StateNotifier<AsyncValue<void>> {
final RestaurantRepository _repository;
RestaurantNotifier(this._repository) : super(const AsyncValue.data(null));
/// 맛집 추가
Future<void> 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<void> updateRestaurant(Restaurant restaurant) async {
state = const AsyncValue.loading();
try {
final updated = Restaurant(
id: restaurant.id,
name: restaurant.name,
category: restaurant.category,
subCategory: restaurant.subCategory,
description: restaurant.description,
phoneNumber: restaurant.phoneNumber,
roadAddress: restaurant.roadAddress,
jibunAddress: restaurant.jibunAddress,
latitude: restaurant.latitude,
longitude: restaurant.longitude,
lastVisitDate: restaurant.lastVisitDate,
source: restaurant.source,
createdAt: restaurant.createdAt,
updatedAt: DateTime.now(),
);
await _repository.updateRestaurant(updated);
state = const AsyncValue.data(null);
} catch (e, stack) {
state = AsyncValue.error(e, stack);
}
}
/// 맛집 삭제
Future<void> 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<void> updateLastVisitDate(String restaurantId, DateTime visitDate) async {
try {
await _repository.updateLastVisitDate(restaurantId, visitDate);
} catch (e, stack) {
state = AsyncValue.error(e, stack);
}
}
/// 네이버 지도 URL로부터 맛집 추가
Future<Restaurant> 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<void> 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<RestaurantNotifier, AsyncValue<void>>((ref) {
final repository = ref.watch(restaurantRepositoryProvider);
return RestaurantNotifier(repository);
});
/// 거리 내 맛집 Provider
final restaurantsWithinDistanceProvider = FutureProvider.family<List<Restaurant>, ({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<List<Restaurant>, int>((ref, days) async {
final repository = ref.watch(restaurantRepositoryProvider);
return repository.getRestaurantsNotVisitedInDays(days);
});
/// 검색어로 맛집 검색 Provider
final searchRestaurantsProvider = FutureProvider.family<List<Restaurant>, String>((ref, query) async {
final repository = ref.watch(restaurantRepositoryProvider);
return repository.searchRestaurants(query);
});
/// 카테고리별 맛집 Provider
final restaurantsByCategoryProvider = FutureProvider.family<List<Restaurant>, String>((ref, category) async {
final repository = ref.watch(restaurantRepositoryProvider);
return repository.getRestaurantsByCategory(category);
});
/// 검색 쿼리 상태 Provider
final searchQueryProvider = StateProvider<String>((ref) => '');
/// 선택된 카테고리 상태 Provider
final selectedCategoryProvider = StateProvider<String?>((ref) => null);
/// 필터링된 맛집 목록 Provider (검색 + 카테고리)
final filteredRestaurantsProvider = StreamProvider<List<Restaurant>>((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;
}
});