feat(app): add manual entry and sharing flows
This commit is contained in:
@@ -9,12 +9,11 @@ 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 =>
|
||||
|
||||
RestaurantRepositoryImpl({NaverSearchService? naverSearchService})
|
||||
: _naverSearchService = naverSearchService ?? NaverSearchService();
|
||||
|
||||
Future<Box<Restaurant>> get _box async =>
|
||||
await Hive.openBox<Restaurant>(_boxName);
|
||||
|
||||
@override
|
||||
@@ -69,7 +68,10 @@ class RestaurantRepositoryImpl implements RestaurantRepository {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateLastVisitDate(String restaurantId, DateTime visitDate) async {
|
||||
Future<void> updateLastVisitDate(
|
||||
String restaurantId,
|
||||
DateTime visitDate,
|
||||
) async {
|
||||
final restaurant = await getRestaurantById(restaurantId);
|
||||
if (restaurant != null) {
|
||||
final updatedRestaurant = Restaurant(
|
||||
@@ -120,7 +122,7 @@ class RestaurantRepositoryImpl implements RestaurantRepository {
|
||||
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);
|
||||
@@ -132,39 +134,68 @@ class RestaurantRepositoryImpl implements RestaurantRepository {
|
||||
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.description?.toLowerCase().contains(lowercaseQuery) ??
|
||||
false) ||
|
||||
restaurant.category.toLowerCase().contains(lowercaseQuery) ||
|
||||
restaurant.roadAddress.toLowerCase().contains(lowercaseQuery);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Restaurant>> searchRestaurantsFromNaver({
|
||||
required String query,
|
||||
double? latitude,
|
||||
double? longitude,
|
||||
}) async {
|
||||
return _naverSearchService.searchNearbyRestaurants(
|
||||
query: query,
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Restaurant> addRestaurantFromUrl(String url) async {
|
||||
return _processRestaurantFromUrl(url, persist: true);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Restaurant> previewRestaurantFromUrl(String url) async {
|
||||
return _processRestaurantFromUrl(url, persist: false);
|
||||
}
|
||||
|
||||
Future<Restaurant> _processRestaurantFromUrl(
|
||||
String url, {
|
||||
required bool persist,
|
||||
}) async {
|
||||
try {
|
||||
// URL 유효성 검증
|
||||
if (!url.contains('naver.com') && !url.contains('naver.me')) {
|
||||
throw Exception('유효하지 않은 네이버 지도 URL입니다.');
|
||||
}
|
||||
|
||||
|
||||
// NaverSearchService로 식당 정보 추출
|
||||
Restaurant restaurant = await _naverSearchService.getRestaurantFromUrl(url);
|
||||
|
||||
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,
|
||||
);
|
||||
|
||||
final detailedRestaurant = await _naverSearchService
|
||||
.searchRestaurantDetails(
|
||||
name: restaurant.name,
|
||||
address: restaurant.roadAddress,
|
||||
latitude: restaurant.latitude,
|
||||
longitude: restaurant.longitude,
|
||||
);
|
||||
|
||||
if (detailedRestaurant != null) {
|
||||
// 기존 정보와 API 검색 결과 병합
|
||||
restaurant = Restaurant(
|
||||
@@ -172,8 +203,10 @@ class RestaurantRepositoryImpl implements RestaurantRepository {
|
||||
name: restaurant.name,
|
||||
category: detailedRestaurant.category,
|
||||
subCategory: detailedRestaurant.subCategory,
|
||||
description: detailedRestaurant.description ?? restaurant.description,
|
||||
phoneNumber: detailedRestaurant.phoneNumber ?? restaurant.phoneNumber,
|
||||
description:
|
||||
detailedRestaurant.description ?? restaurant.description,
|
||||
phoneNumber:
|
||||
detailedRestaurant.phoneNumber ?? restaurant.phoneNumber,
|
||||
roadAddress: detailedRestaurant.roadAddress,
|
||||
jibunAddress: detailedRestaurant.jibunAddress,
|
||||
latitude: detailedRestaurant.latitude,
|
||||
@@ -184,7 +217,8 @@ class RestaurantRepositoryImpl implements RestaurantRepository {
|
||||
updatedAt: DateTime.now(),
|
||||
naverPlaceId: restaurant.naverPlaceId,
|
||||
naverUrl: restaurant.naverUrl,
|
||||
businessHours: detailedRestaurant.businessHours ?? restaurant.businessHours,
|
||||
businessHours:
|
||||
detailedRestaurant.businessHours ?? restaurant.businessHours,
|
||||
lastVisited: restaurant.lastVisited,
|
||||
visitCount: restaurant.visitCount,
|
||||
);
|
||||
@@ -193,50 +227,31 @@ class RestaurantRepositoryImpl implements RestaurantRepository {
|
||||
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}');
|
||||
}
|
||||
|
||||
if (persist) {
|
||||
await _ensureRestaurantIsUnique(restaurant);
|
||||
await addRestaurant(restaurant);
|
||||
}
|
||||
|
||||
// 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 이내
|
||||
},
|
||||
|
||||
return restaurant;
|
||||
} catch (e) {
|
||||
if (e is NaverMapParseException) {
|
||||
throw Exception('네이버 지도 파싱 실패: ${e.message}');
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _ensureRestaurantIsUnique(Restaurant restaurant) async {
|
||||
final restaurants = await getAllRestaurants();
|
||||
|
||||
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: '',
|
||||
@@ -251,20 +266,44 @@ class RestaurantRepositoryImpl implements RestaurantRepository {
|
||||
updatedAt: DateTime.now(),
|
||||
),
|
||||
);
|
||||
|
||||
if (locationDuplicate.id.isNotEmpty) {
|
||||
throw Exception('50m 이내에 동일한 이름의 맛집이 이미 존재합니다: ${locationDuplicate.name}');
|
||||
|
||||
if (addressDuplicate.id.isNotEmpty) {
|
||||
throw Exception('동일한 이름과 주소의 맛집이 이미 존재합니다: ${addressDuplicate.name}');
|
||||
}
|
||||
|
||||
// 새 맛집 추가
|
||||
await addRestaurant(restaurant);
|
||||
|
||||
return restaurant;
|
||||
} catch (e) {
|
||||
if (e is NaverMapParseException) {
|
||||
throw Exception('네이버 지도 파싱 실패: ${e.message}');
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
|
||||
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;
|
||||
},
|
||||
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}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,12 +311,9 @@ class RestaurantRepositoryImpl implements RestaurantRepository {
|
||||
Future<Restaurant?> getRestaurantByNaverPlaceId(String naverPlaceId) async {
|
||||
final restaurants = await getAllRestaurants();
|
||||
try {
|
||||
return restaurants.firstWhere(
|
||||
(r) => r.naverPlaceId == naverPlaceId,
|
||||
);
|
||||
return restaurants.firstWhere((r) => r.naverPlaceId == naverPlaceId);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user