Files
lunchpick/lib/presentation/providers/visit_provider.dart
JiWoong Sul 85fde36157 feat: 초기 프로젝트 설정 및 LunchPick 앱 구현
LunchPick(오늘 뭐 먹Z?) Flutter 앱의 초기 구현입니다.

주요 기능:
- 네이버 지도 연동 맛집 추가
- 랜덤 메뉴 추천 시스템
- 날씨 기반 거리 조정
- 방문 기록 관리
- Bluetooth 맛집 공유
- 다크모드 지원

기술 스택:
- Flutter 3.8.1+
- Riverpod 상태 관리
- Hive 로컬 DB
- Clean Architecture

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-30 19:03:28 +09:00

214 lines
7.4 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;
});