## 성능 최적화 ### main.dart - 앱 초기화 병렬 처리 (Future.wait 활용) - 광고 SDK, Hive 초기화 동시 실행 - Hive Box 오픈 병렬 처리 - 코드 구조화 (_initializeHive, _initializeNotifications) ### visit_provider.dart - allLastVisitDatesProvider 추가 - 리스트 화면에서 N+1 쿼리 방지 - 모든 맛집의 마지막 방문일 일괄 조회 ## UI 개선 ### 각 화면 리팩토링 - AppDimensions 상수 적용 - 스켈레톤 로더 적용 - 코드 정리 및 일관성 개선
297 lines
9.2 KiB
Dart
297 lines
9.2 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);
|
|
});
|
|
|
|
/// 월별 카테고리별 방문 통계 Provider
|
|
final monthlyCategoryVisitStatsProvider =
|
|
FutureProvider.family<Map<String, int>, ({int year, int month})>((
|
|
ref,
|
|
params,
|
|
) async {
|
|
final repository = ref.watch(visitRepositoryProvider);
|
|
final restaurants = await ref.watch(restaurantListProvider.future);
|
|
|
|
final records = await repository.getVisitRecordsByDateRange(
|
|
startDate: DateTime(params.year, params.month, 1),
|
|
endDate: DateTime(params.year, params.month + 1, 0),
|
|
);
|
|
|
|
final categoryCount = <String, int>{};
|
|
for (final record in records) {
|
|
final restaurant = restaurants
|
|
.where((r) => r.id == record.restaurantId)
|
|
.firstOrNull;
|
|
if (restaurant == null) continue;
|
|
|
|
categoryCount[restaurant.category] =
|
|
(categoryCount[restaurant.category] ?? 0) + 1;
|
|
}
|
|
|
|
return categoryCount;
|
|
});
|
|
|
|
/// 방문 기록 관리 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,
|
|
bool isConfirmed = false,
|
|
}) async {
|
|
// 추천 확인 시점으로 방문 시간을 기록
|
|
final visitTime = DateTime.now();
|
|
|
|
await addVisitRecord(
|
|
restaurantId: restaurantId,
|
|
visitDate: visitTime,
|
|
isConfirmed: isConfirmed,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 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);
|
|
});
|
|
|
|
/// 모든 맛집의 마지막 방문일을 한 번에 조회 (리스트 최적화용)
|
|
final allLastVisitDatesProvider =
|
|
FutureProvider<Map<String, DateTime?>>((ref) async {
|
|
final records = await ref.watch(visitRecordsProvider.future);
|
|
|
|
// restaurantId별 가장 최근 방문일 계산
|
|
final lastVisitMap = <String, DateTime>{};
|
|
for (final record in records) {
|
|
final existing = lastVisitMap[record.restaurantId];
|
|
if (existing == null || record.visitDate.isAfter(existing)) {
|
|
lastVisitMap[record.restaurantId] = record.visitDate;
|
|
}
|
|
}
|
|
|
|
return lastVisitMap;
|
|
});
|
|
|
|
/// 기간별 방문 기록 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;
|
|
});
|