LunchPick(오늘 뭐 먹Z?) Flutter 앱의 초기 구현입니다. 주요 기능: - 네이버 지도 연동 맛집 추가 - 랜덤 메뉴 추천 시스템 - 날씨 기반 거리 조정 - 방문 기록 관리 - Bluetooth 맛집 공유 - 다크모드 지원 기술 스택: - Flutter 3.8.1+ - Riverpod 상태 관리 - Hive 로컬 DB - Clean Architecture 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
214 lines
7.4 KiB
Dart
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;
|
|
}); |