Files
lunchpick/lib/presentation/providers/visit_provider.dart
2025-11-19 16:36:39 +09:00

251 lines
7.8 KiB
Dart

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lunchpick/domain/entities/visit_record.dart';
import 'package:lunchpick/domain/repositories/visit_repository.dart';
import 'package:lunchpick/presentation/providers/di_providers.dart';
import 'package:lunchpick/presentation/providers/restaurant_provider.dart';
import 'package:uuid/uuid.dart';
/// 방문 기록 목록 Provider
final visitRecordsProvider = StreamProvider<List<VisitRecord>>((ref) {
final repository = ref.watch(visitRepositoryProvider);
return repository.watchVisitRecords();
});
/// 날짜별 방문 기록 Provider
final visitRecordsByDateProvider =
FutureProvider.family<List<VisitRecord>, DateTime>((ref, date) async {
final repository = ref.watch(visitRepositoryProvider);
return repository.getVisitRecordsByDate(date);
});
/// 맛집별 방문 기록 Provider
final visitRecordsByRestaurantProvider =
FutureProvider.family<List<VisitRecord>, String>((ref, restaurantId) async {
final repository = ref.watch(visitRepositoryProvider);
return repository.getVisitRecordsByRestaurantId(restaurantId);
});
/// 월별 방문 통계 Provider
final monthlyVisitStatsProvider =
FutureProvider.family<Map<String, int>, ({int year, int month})>((
ref,
params,
) async {
final repository = ref.watch(visitRepositoryProvider);
return repository.getMonthlyVisitStats(params.year, params.month);
});
/// 방문 기록 관리 StateNotifier
class VisitNotifier extends StateNotifier<AsyncValue<void>> {
final VisitRepository _repository;
final Ref _ref;
VisitNotifier(this._repository, this._ref)
: super(const AsyncValue.data(null));
/// 방문 기록 추가
Future<void> addVisitRecord({
required String restaurantId,
required DateTime visitDate,
bool isConfirmed = false,
}) async {
state = const AsyncValue.loading();
try {
final visitRecord = VisitRecord(
id: const Uuid().v4(),
restaurantId: restaurantId,
visitDate: visitDate,
isConfirmed: isConfirmed,
createdAt: DateTime.now(),
);
await _repository.addVisitRecord(visitRecord);
// 맛집의 마지막 방문일도 업데이트
final restaurantNotifier = _ref.read(restaurantNotifierProvider.notifier);
await restaurantNotifier.updateLastVisitDate(restaurantId, visitDate);
state = const AsyncValue.data(null);
} catch (e, stack) {
state = AsyncValue.error(e, stack);
}
}
/// 방문 확인
Future<void> confirmVisit(String visitRecordId) async {
state = const AsyncValue.loading();
try {
await _repository.confirmVisit(visitRecordId);
state = const AsyncValue.data(null);
} catch (e, stack) {
state = AsyncValue.error(e, stack);
}
}
/// 방문 기록 삭제
Future<void> deleteVisitRecord(String id) async {
state = const AsyncValue.loading();
try {
await _repository.deleteVisitRecord(id);
state = const AsyncValue.data(null);
} catch (e, stack) {
state = AsyncValue.error(e, stack);
}
}
/// 추천 후 자동 방문 기록 생성
Future<void> createVisitFromRecommendation({
required String restaurantId,
required DateTime recommendationTime,
}) async {
// 추천 시간으로부터 1.5시간 후를 방문 시간으로 설정
final visitTime = recommendationTime.add(const Duration(minutes: 90));
await addVisitRecord(
restaurantId: restaurantId,
visitDate: visitTime,
isConfirmed: false, // 나중에 확인 필요
);
}
}
/// VisitNotifier Provider
final visitNotifierProvider =
StateNotifierProvider<VisitNotifier, AsyncValue<void>>((ref) {
final repository = ref.watch(visitRepositoryProvider);
return VisitNotifier(repository, ref);
});
/// 특정 맛집의 마지막 방문일 Provider
final lastVisitDateProvider = FutureProvider.family<DateTime?, String>((
ref,
restaurantId,
) async {
final repository = ref.watch(visitRepositoryProvider);
return repository.getLastVisitDate(restaurantId);
});
/// 기간별 방문 기록 Provider
final visitRecordsByPeriodProvider =
FutureProvider.family<
List<VisitRecord>,
({DateTime startDate, DateTime endDate})
>((ref, params) async {
final allRecords = await ref.watch(visitRecordsProvider.future);
return allRecords.where((record) {
return record.visitDate.isAfter(params.startDate) &&
record.visitDate.isBefore(
params.endDate.add(const Duration(days: 1)),
);
}).toList()..sort((a, b) => b.visitDate.compareTo(a.visitDate));
});
/// 주간 방문 통계 Provider (최근 7일)
final weeklyVisitStatsProvider = FutureProvider<Map<String, int>>((ref) async {
final now = DateTime.now();
final startOfWeek = DateTime(
now.year,
now.month,
now.day,
).subtract(const Duration(days: 6));
final records = await ref.watch(
visitRecordsByPeriodProvider((startDate: startOfWeek, endDate: now)).future,
);
final stats = <String, int>{};
for (var i = 0; i < 7; i++) {
final date = startOfWeek.add(Duration(days: i));
final dateKey = '${date.month}/${date.day}';
stats[dateKey] = records
.where(
(r) =>
r.visitDate.year == date.year &&
r.visitDate.month == date.month &&
r.visitDate.day == date.day,
)
.length;
}
return stats;
});
/// 자주 방문하는 맛집 Provider (상위 10개)
final frequentRestaurantsProvider =
FutureProvider<List<({String restaurantId, int visitCount})>>((ref) async {
final allRecords = await ref.watch(visitRecordsProvider.future);
final visitCounts = <String, int>{};
for (final record in allRecords) {
visitCounts[record.restaurantId] =
(visitCounts[record.restaurantId] ?? 0) + 1;
}
final sorted = visitCounts.entries.toList()
..sort((a, b) => b.value.compareTo(a.value));
return sorted
.take(10)
.map((e) => (restaurantId: e.key, visitCount: e.value))
.toList();
});
/// 방문 기록 정렬 옵션
enum VisitSortOption {
dateDesc, // 최신순
dateAsc, // 오래된순
restaurant, // 맛집별
}
/// 정렬된 방문 기록 Provider
final sortedVisitRecordsProvider =
Provider.family<AsyncValue<List<VisitRecord>>, VisitSortOption>((
ref,
sortOption,
) {
final recordsAsync = ref.watch(visitRecordsProvider);
return recordsAsync.when(
data: (records) {
final sorted = List<VisitRecord>.from(records);
switch (sortOption) {
case VisitSortOption.dateDesc:
sorted.sort((a, b) => b.visitDate.compareTo(a.visitDate));
break;
case VisitSortOption.dateAsc:
sorted.sort((a, b) => a.visitDate.compareTo(b.visitDate));
break;
case VisitSortOption.restaurant:
sorted.sort((a, b) => a.restaurantId.compareTo(b.restaurantId));
break;
}
return AsyncValue.data(sorted);
},
loading: () => const AsyncValue.loading(),
error: (error, stack) => AsyncValue.error(error, stack),
);
});
/// 카테고리별 방문 통계 Provider
final categoryVisitStatsProvider = FutureProvider<Map<String, int>>((
ref,
) async {
final allRecords = await ref.watch(visitRecordsProvider.future);
final restaurantsAsync = await ref.watch(restaurantListProvider.future);
final categoryCount = <String, int>{};
for (final record in allRecords) {
final restaurant = restaurantsAsync
.where((r) => r.id == record.restaurantId)
.firstOrNull;
if (restaurant != null) {
categoryCount[restaurant.category] =
(categoryCount[restaurant.category] ?? 0) + 1;
}
}
return categoryCount;
});