1. placeId 기반 중복 체크 제거 - 이전 대화에서 명확히 한 대로 placeId는 매칭에 사용하지 않음 2. 주소 기반 매칭 개선 - 주소가 있을 때만 주소 기반 중복 체크 수행 3. 위치 기반 매칭 추가 - 50m 이내 동일한 이름의 맛집 중복 체크 추가 4. 검색 결과 선택 로직 개선 - 주소가 없을 때 가장 가까운 거리의 업체 선택 5. 카테고리 필터 버그 수정 - 카테고리 표시명과 실제 값 불일치 문제 해결 - 부분 일치 및 정규화된 비교 지원 6. 빈 상태 메시지 개선 - 필터링 중일 때 적절한 안내 메시지 표시 - 필터 초기화 버튼 추가 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
283 lines
9.6 KiB
Dart
283 lines
9.6 KiB
Dart
import 'package:hive_flutter/hive_flutter.dart';
|
|
import 'package:lunchpick/domain/entities/restaurant.dart';
|
|
import 'package:lunchpick/domain/repositories/restaurant_repository.dart';
|
|
import 'package:lunchpick/core/utils/distance_calculator.dart';
|
|
import 'package:lunchpick/data/datasources/remote/naver_search_service.dart';
|
|
import 'package:lunchpick/data/datasources/remote/naver_map_parser.dart';
|
|
import 'package:lunchpick/core/constants/api_keys.dart';
|
|
|
|
class RestaurantRepositoryImpl implements RestaurantRepository {
|
|
static const String _boxName = 'restaurants';
|
|
final NaverSearchService _naverSearchService;
|
|
|
|
RestaurantRepositoryImpl({
|
|
NaverSearchService? naverSearchService,
|
|
}) : _naverSearchService = naverSearchService ?? NaverSearchService();
|
|
|
|
Future<Box<Restaurant>> get _box async =>
|
|
await Hive.openBox<Restaurant>(_boxName);
|
|
|
|
@override
|
|
Future<List<Restaurant>> getAllRestaurants() async {
|
|
final box = await _box;
|
|
return box.values.toList();
|
|
}
|
|
|
|
@override
|
|
Future<Restaurant?> getRestaurantById(String id) async {
|
|
final box = await _box;
|
|
return box.get(id);
|
|
}
|
|
|
|
@override
|
|
Future<void> addRestaurant(Restaurant restaurant) async {
|
|
final box = await _box;
|
|
await box.put(restaurant.id, restaurant);
|
|
}
|
|
|
|
@override
|
|
Future<void> updateRestaurant(Restaurant restaurant) async {
|
|
final box = await _box;
|
|
await box.put(restaurant.id, restaurant);
|
|
}
|
|
|
|
@override
|
|
Future<void> deleteRestaurant(String id) async {
|
|
final box = await _box;
|
|
await box.delete(id);
|
|
}
|
|
|
|
@override
|
|
Future<List<Restaurant>> getRestaurantsByCategory(String category) async {
|
|
final restaurants = await getAllRestaurants();
|
|
return restaurants.where((r) => r.category == category).toList();
|
|
}
|
|
|
|
@override
|
|
Future<List<String>> getAllCategories() async {
|
|
final restaurants = await getAllRestaurants();
|
|
final categories = restaurants.map((r) => r.category).toSet().toList();
|
|
categories.sort();
|
|
return categories;
|
|
}
|
|
|
|
@override
|
|
Stream<List<Restaurant>> watchRestaurants() async* {
|
|
final box = await _box;
|
|
yield box.values.toList();
|
|
yield* box.watch().map((_) => box.values.toList());
|
|
}
|
|
|
|
@override
|
|
Future<void> updateLastVisitDate(String restaurantId, DateTime visitDate) async {
|
|
final restaurant = await getRestaurantById(restaurantId);
|
|
if (restaurant != null) {
|
|
final updatedRestaurant = 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: visitDate,
|
|
source: restaurant.source,
|
|
createdAt: restaurant.createdAt,
|
|
updatedAt: DateTime.now(),
|
|
naverPlaceId: restaurant.naverPlaceId,
|
|
naverUrl: restaurant.naverUrl,
|
|
businessHours: restaurant.businessHours,
|
|
lastVisited: visitDate,
|
|
visitCount: restaurant.visitCount + 1,
|
|
);
|
|
await updateRestaurant(updatedRestaurant);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<List<Restaurant>> getRestaurantsWithinDistance({
|
|
required double userLatitude,
|
|
required double userLongitude,
|
|
required double maxDistanceInMeters,
|
|
}) async {
|
|
final restaurants = await getAllRestaurants();
|
|
return restaurants.where((restaurant) {
|
|
final distanceInKm = DistanceCalculator.calculateDistance(
|
|
lat1: userLatitude,
|
|
lon1: userLongitude,
|
|
lat2: restaurant.latitude,
|
|
lon2: restaurant.longitude,
|
|
);
|
|
final distanceInMeters = distanceInKm * 1000;
|
|
return distanceInMeters <= maxDistanceInMeters;
|
|
}).toList();
|
|
}
|
|
|
|
@override
|
|
Future<List<Restaurant>> getRestaurantsNotVisitedInDays(int days) async {
|
|
final restaurants = await getAllRestaurants();
|
|
final cutoffDate = DateTime.now().subtract(Duration(days: days));
|
|
|
|
return restaurants.where((restaurant) {
|
|
if (restaurant.lastVisitDate == null) return true;
|
|
return restaurant.lastVisitDate!.isBefore(cutoffDate);
|
|
}).toList();
|
|
}
|
|
|
|
@override
|
|
Future<List<Restaurant>> searchRestaurants(String query) async {
|
|
if (query.isEmpty) {
|
|
return await getAllRestaurants();
|
|
}
|
|
|
|
final restaurants = await getAllRestaurants();
|
|
final lowercaseQuery = query.toLowerCase();
|
|
|
|
return restaurants.where((restaurant) {
|
|
return restaurant.name.toLowerCase().contains(lowercaseQuery) ||
|
|
(restaurant.description?.toLowerCase().contains(lowercaseQuery) ?? false) ||
|
|
restaurant.category.toLowerCase().contains(lowercaseQuery) ||
|
|
restaurant.roadAddress.toLowerCase().contains(lowercaseQuery);
|
|
}).toList();
|
|
}
|
|
|
|
@override
|
|
Future<Restaurant> addRestaurantFromUrl(String url) async {
|
|
try {
|
|
// URL 유효성 검증
|
|
if (!url.contains('naver.com') && !url.contains('naver.me')) {
|
|
throw Exception('유효하지 않은 네이버 지도 URL입니다.');
|
|
}
|
|
|
|
// NaverSearchService로 식당 정보 추출
|
|
Restaurant restaurant = await _naverSearchService.getRestaurantFromUrl(url);
|
|
|
|
// API 키가 설정되어 있으면 추가 정보 검색
|
|
if (ApiKeys.areKeysConfigured() && restaurant.name != '네이버 지도 장소') {
|
|
try {
|
|
final detailedRestaurant = await _naverSearchService.searchRestaurantDetails(
|
|
name: restaurant.name,
|
|
address: restaurant.roadAddress,
|
|
latitude: restaurant.latitude,
|
|
longitude: restaurant.longitude,
|
|
);
|
|
|
|
if (detailedRestaurant != null) {
|
|
// 기존 정보와 API 검색 결과 병합
|
|
restaurant = Restaurant(
|
|
id: restaurant.id,
|
|
name: restaurant.name,
|
|
category: detailedRestaurant.category,
|
|
subCategory: detailedRestaurant.subCategory,
|
|
description: detailedRestaurant.description ?? restaurant.description,
|
|
phoneNumber: detailedRestaurant.phoneNumber ?? restaurant.phoneNumber,
|
|
roadAddress: detailedRestaurant.roadAddress,
|
|
jibunAddress: detailedRestaurant.jibunAddress,
|
|
latitude: detailedRestaurant.latitude,
|
|
longitude: detailedRestaurant.longitude,
|
|
lastVisitDate: restaurant.lastVisitDate,
|
|
source: DataSource.NAVER,
|
|
createdAt: restaurant.createdAt,
|
|
updatedAt: DateTime.now(),
|
|
naverPlaceId: restaurant.naverPlaceId,
|
|
naverUrl: restaurant.naverUrl,
|
|
businessHours: detailedRestaurant.businessHours ?? restaurant.businessHours,
|
|
lastVisited: restaurant.lastVisited,
|
|
visitCount: restaurant.visitCount,
|
|
);
|
|
}
|
|
} catch (e) {
|
|
print('API 검색 실패, 스크래핑된 정보만 사용: $e');
|
|
}
|
|
}
|
|
|
|
// 중복 체크 개선
|
|
final restaurants = await getAllRestaurants();
|
|
|
|
// 1. 주소 기반 중복 체크
|
|
if (restaurant.roadAddress.isNotEmpty || restaurant.jibunAddress.isNotEmpty) {
|
|
final addressDuplicate = restaurants.firstWhere(
|
|
(r) => r.name == restaurant.name &&
|
|
(r.roadAddress == restaurant.roadAddress ||
|
|
r.jibunAddress == restaurant.jibunAddress),
|
|
orElse: () => Restaurant(
|
|
id: '',
|
|
name: '',
|
|
category: '',
|
|
subCategory: '',
|
|
roadAddress: '',
|
|
jibunAddress: '',
|
|
latitude: 0,
|
|
longitude: 0,
|
|
source: DataSource.USER_INPUT,
|
|
createdAt: DateTime.now(),
|
|
updatedAt: DateTime.now(),
|
|
),
|
|
);
|
|
|
|
if (addressDuplicate.id.isNotEmpty) {
|
|
throw Exception('동일한 이름과 주소의 맛집이 이미 존재합니다: ${addressDuplicate.name}');
|
|
}
|
|
}
|
|
|
|
// 2. 위치 기반 중복 체크 (50m 이내 같은 이름)
|
|
final locationDuplicate = restaurants.firstWhere(
|
|
(r) {
|
|
if (r.name != restaurant.name) return false;
|
|
|
|
final distanceInKm = DistanceCalculator.calculateDistance(
|
|
lat1: r.latitude,
|
|
lon1: r.longitude,
|
|
lat2: restaurant.latitude,
|
|
lon2: restaurant.longitude,
|
|
);
|
|
final distanceInMeters = distanceInKm * 1000;
|
|
return distanceInMeters < 50; // 50m 이내
|
|
},
|
|
orElse: () => Restaurant(
|
|
id: '',
|
|
name: '',
|
|
category: '',
|
|
subCategory: '',
|
|
roadAddress: '',
|
|
jibunAddress: '',
|
|
latitude: 0,
|
|
longitude: 0,
|
|
source: DataSource.USER_INPUT,
|
|
createdAt: DateTime.now(),
|
|
updatedAt: DateTime.now(),
|
|
),
|
|
);
|
|
|
|
if (locationDuplicate.id.isNotEmpty) {
|
|
throw Exception('50m 이내에 동일한 이름의 맛집이 이미 존재합니다: ${locationDuplicate.name}');
|
|
}
|
|
|
|
// 새 맛집 추가
|
|
await addRestaurant(restaurant);
|
|
|
|
return restaurant;
|
|
} catch (e) {
|
|
if (e is NaverMapParseException) {
|
|
throw Exception('네이버 지도 파싱 실패: ${e.message}');
|
|
}
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<Restaurant?> getRestaurantByNaverPlaceId(String naverPlaceId) async {
|
|
final restaurants = await getAllRestaurants();
|
|
try {
|
|
return restaurants.firstWhere(
|
|
(r) => r.naverPlaceId == naverPlaceId,
|
|
);
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
} |