feat(app): stabilize recommendation flow
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:lunchpick/core/utils/app_logger.dart';
|
||||
@@ -69,13 +70,31 @@ final currentLocationProvider = FutureProvider<Position?>((ref) async {
|
||||
});
|
||||
|
||||
/// 위치 스트림 Provider
|
||||
final locationStreamProvider = StreamProvider<Position>((ref) {
|
||||
return Geolocator.getPositionStream(
|
||||
locationSettings: const LocationSettings(
|
||||
accuracy: LocationAccuracy.high,
|
||||
distanceFilter: 10, // 10미터 이상 이동 시 업데이트
|
||||
),
|
||||
);
|
||||
final locationStreamProvider = StreamProvider<Position>((ref) async* {
|
||||
if (kIsWeb) {
|
||||
AppLogger.debug('[location] web detected, emit fallback immediately');
|
||||
yield defaultPosition();
|
||||
return;
|
||||
}
|
||||
|
||||
final status = await Permission.location.status;
|
||||
if (!status.isGranted) {
|
||||
AppLogger.debug('[location] permission not granted, emit fallback');
|
||||
yield defaultPosition();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
yield* Geolocator.getPositionStream(
|
||||
locationSettings: const LocationSettings(
|
||||
accuracy: LocationAccuracy.high,
|
||||
distanceFilter: 10, // 10미터 이상 이동 시 업데이트
|
||||
),
|
||||
);
|
||||
} catch (_) {
|
||||
AppLogger.error('[location] position stream failed, emit fallback');
|
||||
yield defaultPosition();
|
||||
}
|
||||
});
|
||||
|
||||
/// 초기 3초 내 위치를 가져오지 못하면 기본 좌표를 우선 반환하고,
|
||||
@@ -83,20 +102,30 @@ final locationStreamProvider = StreamProvider<Position>((ref) {
|
||||
final currentLocationWithFallbackProvider = StreamProvider<Position>((
|
||||
ref,
|
||||
) async* {
|
||||
final initial = await Future.any([
|
||||
ref
|
||||
.watch(currentLocationProvider.future)
|
||||
.then((pos) => pos ?? defaultPosition()),
|
||||
Future<Position>.delayed(
|
||||
const Duration(seconds: 3),
|
||||
() => defaultPosition(),
|
||||
),
|
||||
]).catchError((_) => defaultPosition());
|
||||
AppLogger.debug('[location] emit fallback immediately (safe start)');
|
||||
// 웹/권한 거부 상황에서는 즉시 기본 좌표를 먼저 흘려보내 리스트 로딩을 막는다.
|
||||
final fallback = defaultPosition();
|
||||
yield fallback;
|
||||
|
||||
yield initial;
|
||||
final initial = await Future.any([
|
||||
ref.watch(currentLocationProvider.future).then((pos) => pos ?? fallback),
|
||||
Future<Position>.delayed(const Duration(seconds: 3), () => fallback),
|
||||
]).catchError((_) => fallback);
|
||||
|
||||
if (initial.latitude != fallback.latitude ||
|
||||
initial.longitude != fallback.longitude) {
|
||||
AppLogger.debug(
|
||||
'[location] resolved initial position: '
|
||||
'${initial.latitude}, ${initial.longitude}',
|
||||
);
|
||||
yield initial;
|
||||
} else {
|
||||
AppLogger.debug('[location] initial resolved to fallback');
|
||||
}
|
||||
|
||||
yield* ref.watch(locationStreamProvider.stream).handleError((_) {
|
||||
// 스트림 오류는 무시하고 마지막 위치를 유지
|
||||
AppLogger.error('[location] stream error, keeping last position');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -140,6 +140,7 @@ class RecommendationNotifier extends StateNotifier<AsyncValue<Restaurant?>> {
|
||||
Future<RecommendationRecord> saveRecommendationRecord(
|
||||
Restaurant restaurant, {
|
||||
DateTime? recommendationTime,
|
||||
bool visited = false,
|
||||
}) async {
|
||||
final now = DateTime.now();
|
||||
|
||||
@@ -147,7 +148,7 @@ class RecommendationNotifier extends StateNotifier<AsyncValue<Restaurant?>> {
|
||||
id: const Uuid().v4(),
|
||||
restaurantId: restaurant.id,
|
||||
recommendationDate: recommendationTime ?? now,
|
||||
visited: false,
|
||||
visited: visited,
|
||||
createdAt: now,
|
||||
);
|
||||
|
||||
@@ -172,7 +173,11 @@ class RecommendationNotifier extends StateNotifier<AsyncValue<Restaurant?>> {
|
||||
await visitNotifier.createVisitFromRecommendation(
|
||||
restaurantId: recommendation.restaurantId,
|
||||
recommendationTime: recommendation.recommendationDate,
|
||||
isConfirmed: true,
|
||||
);
|
||||
|
||||
// 방문 기록을 만들었으므로 추천 기록은 숨김 처리
|
||||
await _repository.deleteRecommendationRecord(recommendationId);
|
||||
} catch (e, stack) {
|
||||
state = AsyncValue.error(e, stack);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:lunchpick/core/utils/category_mapper.dart';
|
||||
import 'package:lunchpick/core/utils/app_logger.dart';
|
||||
import 'package:lunchpick/domain/entities/restaurant.dart';
|
||||
import 'package:lunchpick/domain/repositories/restaurant_repository.dart';
|
||||
import 'package:lunchpick/presentation/providers/di_providers.dart';
|
||||
@@ -14,16 +15,28 @@ final restaurantListProvider = StreamProvider<List<Restaurant>>((ref) {
|
||||
});
|
||||
|
||||
/// 거리 정보를 포함한 맛집 목록 Provider (현재 위치 기반)
|
||||
/// StreamProvider 의존으로 초기 이벤트를 놓치는 문제를 피하기 위해
|
||||
/// 기존 리스트 스트림의 AsyncValue를 그대로 전달하며 정렬만 적용한다.
|
||||
final sortedRestaurantsByDistanceProvider =
|
||||
StreamProvider<List<({Restaurant restaurant, double? distanceKm})>>((ref) {
|
||||
final restaurantsStream = ref.watch(restaurantListProvider.stream);
|
||||
final positionAsync = ref.watch(currentLocationProvider);
|
||||
Provider<AsyncValue<List<({Restaurant restaurant, double? distanceKm})>>>((
|
||||
ref,
|
||||
) {
|
||||
final restaurantsAsync = ref.watch(restaurantListProvider);
|
||||
final positionAsync = ref.watch(currentLocationWithFallbackProvider);
|
||||
final position = positionAsync.maybeWhen(
|
||||
data: (pos) => pos ?? defaultPosition(),
|
||||
data: (pos) => pos,
|
||||
orElse: () => defaultPosition(),
|
||||
);
|
||||
|
||||
return restaurantsStream.map((restaurants) {
|
||||
AppLogger.debug(
|
||||
'[restaurant_list] position ready for sorting: '
|
||||
'${position.latitude}, ${position.longitude}',
|
||||
);
|
||||
|
||||
return restaurantsAsync.whenData((restaurants) {
|
||||
AppLogger.debug(
|
||||
'[restaurant_list] incoming restaurants: ${restaurants.length}',
|
||||
);
|
||||
final sorted =
|
||||
restaurants.map<({Restaurant restaurant, double? distanceKm})>((r) {
|
||||
final distanceKm = DistanceCalculator.calculateDistance(
|
||||
@@ -38,6 +51,10 @@ final sortedRestaurantsByDistanceProvider =
|
||||
b.distanceKm ?? double.infinity,
|
||||
),
|
||||
);
|
||||
AppLogger.debug(
|
||||
'[restaurant_list] sorted list emitted, first distanceKm: '
|
||||
'${sorted.isNotEmpty ? sorted.first.distanceKm?.toStringAsFixed(3) : 'none'}',
|
||||
);
|
||||
return sorted;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -100,6 +100,7 @@ class VisitNotifier extends StateNotifier<AsyncValue<void>> {
|
||||
Future<void> createVisitFromRecommendation({
|
||||
required String restaurantId,
|
||||
required DateTime recommendationTime,
|
||||
bool isConfirmed = false,
|
||||
}) async {
|
||||
// 추천 시간으로부터 1.5시간 후를 방문 시간으로 설정
|
||||
final visitTime = recommendationTime.add(const Duration(minutes: 90));
|
||||
@@ -107,7 +108,7 @@ class VisitNotifier extends StateNotifier<AsyncValue<void>> {
|
||||
await addVisitRecord(
|
||||
restaurantId: restaurantId,
|
||||
visitDate: visitTime,
|
||||
isConfirmed: false, // 나중에 확인 필요
|
||||
isConfirmed: isConfirmed,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user