fix: 맛집 중복 체크 및 카테고리 필터 로직 개선
1. placeId 기반 중복 체크 제거 - 이전 대화에서 명확히 한 대로 placeId는 매칭에 사용하지 않음 2. 주소 기반 매칭 개선 - 주소가 있을 때만 주소 기반 중복 체크 수행 3. 위치 기반 매칭 추가 - 50m 이내 동일한 이름의 맛집 중복 체크 추가 4. 검색 결과 선택 로직 개선 - 주소가 없을 때 가장 가까운 거리의 업체 선택 5. 카테고리 필터 버그 수정 - 카테고리 표시명과 실제 값 불일치 문제 해결 - 부분 일치 및 정규화된 비교 지원 6. 빈 상태 메시지 개선 - 필터링 중일 때 적절한 안내 메시지 표시 - 필터 초기화 버튼 추가 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import '../../api/naver_api_client.dart';
|
||||
import '../../api/naver/naver_local_search_api.dart';
|
||||
import '../../../domain/entities/restaurant.dart';
|
||||
import '../../../core/errors/network_exceptions.dart';
|
||||
import '../../../core/utils/distance_calculator.dart';
|
||||
import 'naver_map_parser.dart';
|
||||
|
||||
/// 네이버 검색 서비스
|
||||
@@ -111,8 +112,14 @@ class NaverSearchService {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 가장 유사한 결과 찾기
|
||||
final bestMatch = _findBestMatch(name, searchResults);
|
||||
// 가장 유사한 결과 찾기 (주소가 없으면 거리 기반 선택 포함)
|
||||
final bestMatch = _findBestMatch(
|
||||
name,
|
||||
searchResults,
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
address: address,
|
||||
);
|
||||
|
||||
if (bestMatch != null) {
|
||||
final restaurant = bestMatch.toRestaurant(id: _uuid.v4());
|
||||
@@ -172,8 +179,11 @@ class NaverSearchService {
|
||||
/// 가장 유사한 검색 결과 찾기
|
||||
NaverLocalSearchResult? _findBestMatch(
|
||||
String targetName,
|
||||
List<NaverLocalSearchResult> results,
|
||||
) {
|
||||
List<NaverLocalSearchResult> results, {
|
||||
double? latitude,
|
||||
double? longitude,
|
||||
String? address,
|
||||
}) {
|
||||
if (results.isEmpty) return null;
|
||||
|
||||
// 정확히 일치하는 결과 우선
|
||||
@@ -186,6 +196,28 @@ class NaverSearchService {
|
||||
return exactMatch;
|
||||
}
|
||||
|
||||
// 주소가 없고 위치 정보가 있는 경우 - 가장 가까운 업체 선택
|
||||
if ((address == null || address.isEmpty) && latitude != null && longitude != null) {
|
||||
NaverLocalSearchResult? closestResult;
|
||||
double minDistance = double.infinity;
|
||||
|
||||
for (final result in results) {
|
||||
final distance = DistanceCalculator.calculateDistance(
|
||||
lat1: latitude,
|
||||
lon1: longitude,
|
||||
lat2: result.latitude,
|
||||
lon2: result.longitude,
|
||||
);
|
||||
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
closestResult = result;
|
||||
}
|
||||
}
|
||||
|
||||
return closestResult ?? results.first;
|
||||
}
|
||||
|
||||
// 유사도 계산 (간단한 버전)
|
||||
NaverLocalSearchResult? bestMatch;
|
||||
double bestScore = 0.0;
|
||||
@@ -239,9 +271,18 @@ class NaverSearchService {
|
||||
@visibleForTesting
|
||||
NaverLocalSearchResult? findBestMatchForTesting(
|
||||
String targetName,
|
||||
List<NaverLocalSearchResult> results,
|
||||
) {
|
||||
return _findBestMatch(targetName, results);
|
||||
List<NaverLocalSearchResult> results, {
|
||||
double? latitude,
|
||||
double? longitude,
|
||||
String? address,
|
||||
}) {
|
||||
return _findBestMatch(
|
||||
targetName,
|
||||
results,
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
address: address,
|
||||
);
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
|
||||
@@ -194,17 +194,12 @@ class RestaurantRepositoryImpl implements RestaurantRepository {
|
||||
}
|
||||
}
|
||||
|
||||
// 중복 체크 - Place ID가 있는 경우
|
||||
if (restaurant.naverPlaceId != null) {
|
||||
final existingRestaurant = await getRestaurantByNaverPlaceId(restaurant.naverPlaceId!);
|
||||
if (existingRestaurant != null) {
|
||||
throw Exception('이미 등록된 맛집입니다: ${existingRestaurant.name}');
|
||||
}
|
||||
}
|
||||
|
||||
// 중복 체크 - 이름과 주소로 추가 확인
|
||||
// 중복 체크 개선
|
||||
final restaurants = await getAllRestaurants();
|
||||
final duplicate = restaurants.firstWhere(
|
||||
|
||||
// 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),
|
||||
@@ -223,8 +218,42 @@ class RestaurantRepositoryImpl implements RestaurantRepository {
|
||||
),
|
||||
);
|
||||
|
||||
if (duplicate.id.isNotEmpty) {
|
||||
throw Exception('동일한 이름과 주소의 맛집이 이미 존재합니다: ${duplicate.name}');
|
||||
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}');
|
||||
}
|
||||
|
||||
// 새 맛집 추가
|
||||
|
||||
@@ -150,25 +150,50 @@ class _RestaurantListScreenState extends ConsumerState<RestaurantListScreen> {
|
||||
}
|
||||
|
||||
Widget _buildEmptyState(bool isDark) {
|
||||
final selectedCategory = ref.watch(selectedCategoryProvider);
|
||||
final searchQuery = ref.watch(searchQueryProvider);
|
||||
final isFiltering = selectedCategory != null || searchQuery.isNotEmpty;
|
||||
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.restaurant_menu,
|
||||
isFiltering ? Icons.search_off : Icons.restaurant_menu,
|
||||
size: 80,
|
||||
color: isDark ? AppColors.darkTextSecondary : AppColors.lightTextSecondary,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'아직 등록된 맛집이 없어요',
|
||||
isFiltering
|
||||
? '조건에 맞는 맛집이 없어요'
|
||||
: '아직 등록된 맛집이 없어요',
|
||||
style: AppTypography.heading2(isDark),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'+ 버튼을 눌러 맛집을 추가해보세요',
|
||||
isFiltering
|
||||
? selectedCategory != null
|
||||
? '선택한 카테고리에 해당하는 맛집이 없습니다'
|
||||
: '검색 결과가 없습니다'
|
||||
: '+ 버튼을 눌러 맛집을 추가해보세요',
|
||||
style: AppTypography.body2(isDark),
|
||||
),
|
||||
if (isFiltering) ...[
|
||||
const SizedBox(height: 16),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
ref.read(selectedCategoryProvider.notifier).state = null;
|
||||
ref.read(searchQueryProvider.notifier).state = '';
|
||||
},
|
||||
child: Text(
|
||||
'필터 초기화',
|
||||
style: TextStyle(
|
||||
color: AppColors.lightPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
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';
|
||||
@@ -207,7 +208,13 @@ final filteredRestaurantsProvider = StreamProvider<List<Restaurant>>((ref) async
|
||||
// 카테고리 필터 적용
|
||||
if (selectedCategory != null) {
|
||||
filtered = filtered.where((restaurant) {
|
||||
return restaurant.category == selectedCategory;
|
||||
// 정확한 일치 또는 부분 일치 확인
|
||||
// restaurant.category가 "음식점>한식>백반/한정식" 형태일 때
|
||||
// selectedCategory가 "백반/한정식"이면 매칭
|
||||
return restaurant.category == selectedCategory ||
|
||||
restaurant.category.contains(selectedCategory) ||
|
||||
CategoryMapper.normalizeNaverCategory(restaurant.category, restaurant.subCategory) == selectedCategory ||
|
||||
CategoryMapper.getDisplayName(restaurant.category) == selectedCategory;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user