Files
lunchpick/lib/presentation/providers/debug_test_data_provider.dart
JiWoong Sul df4c34194c feat(debug): add preview toggles and auto-close ads
- 기록/통계 탭에 디버그 토글 배너 추가 및 테스트 데이터 주입 로직 상태화\n- 리스트 공유 화면에 디버그 프리뷰 토글, 광고 관문, 디버그 전송 흐름 반영\n- 모의 전면 광고는 대기 시간 종료 시 자동 완료되도록 변경\n- AGENTS.md에 코멘트는 한국어로 작성 규칙 명시\n\n테스트: flutter analyze; flutter test
2025-12-02 15:24:34 +09:00

389 lines
12 KiB
Dart

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lunchpick/domain/entities/recommendation_record.dart';
import 'package:lunchpick/domain/entities/restaurant.dart';
import 'package:lunchpick/domain/entities/visit_record.dart';
import 'package:lunchpick/presentation/providers/di_providers.dart';
class DebugTestDataState {
final bool isEnabled;
final bool isProcessing;
final String? errorMessage;
const DebugTestDataState({
this.isEnabled = false,
this.isProcessing = false,
this.errorMessage,
});
DebugTestDataState copyWith({
bool? isEnabled,
bool? isProcessing,
String? errorMessage,
}) {
return DebugTestDataState(
isEnabled: isEnabled ?? this.isEnabled,
isProcessing: isProcessing ?? this.isProcessing,
errorMessage: errorMessage,
);
}
}
class DebugTestDataNotifier extends StateNotifier<DebugTestDataState> {
DebugTestDataNotifier(this._ref) : super(const DebugTestDataState());
final Ref _ref;
static const String _idPrefix = 'debug-preview-';
Future<void> initialize() async {
if (state.isProcessing) return;
state = state.copyWith(isProcessing: true, errorMessage: null);
try {
final hasDebugData = await _hasExistingDebugData();
state = state.copyWith(isEnabled: hasDebugData, isProcessing: false);
} catch (e) {
state = state.copyWith(isProcessing: false, errorMessage: e.toString());
}
}
Future<void> enableTestData() async {
if (state.isProcessing) return;
state = state.copyWith(isProcessing: true, errorMessage: null);
try {
await _clearDebugData();
await _seedDebugData();
state = state.copyWith(isEnabled: true, isProcessing: false);
} catch (e) {
state = state.copyWith(isProcessing: false, errorMessage: e.toString());
}
}
Future<void> disableTestData() async {
if (state.isProcessing) return;
state = state.copyWith(isProcessing: true, errorMessage: null);
try {
await _clearDebugData();
state = state.copyWith(isEnabled: false, isProcessing: false);
} catch (e) {
state = state.copyWith(isProcessing: false, errorMessage: e.toString());
}
}
Future<void> _seedDebugData() async {
final restaurantRepo = _ref.read(restaurantRepositoryProvider);
final visitRepo = _ref.read(visitRepositoryProvider);
final recommendationRepo = _ref.read(recommendationRepositoryProvider);
final samples = _buildDebugSamples();
for (final sample in samples) {
await restaurantRepo.addRestaurant(sample.restaurant);
}
for (final sample in samples) {
for (final visit in sample.visits) {
await visitRepo.addVisitRecord(visit);
}
for (final reco in sample.recommendations) {
await recommendationRepo.addRecommendationRecord(reco);
}
}
}
Future<void> _clearDebugData() async {
final visitRepo = _ref.read(visitRepositoryProvider);
final recommendationRepo = _ref.read(recommendationRepositoryProvider);
final restaurantRepo = _ref.read(restaurantRepositoryProvider);
final visits = await visitRepo.getAllVisitRecords();
for (final visit in visits.where((v) => _isDebugId(v.id))) {
await visitRepo.deleteVisitRecord(visit.id);
}
final recos = await recommendationRepo.getAllRecommendationRecords();
for (final reco in recos.where((r) => _isDebugId(r.id))) {
await recommendationRepo.deleteRecommendationRecord(reco.id);
}
final restaurants = await restaurantRepo.getAllRestaurants();
for (final restaurant in restaurants.where((r) => _isDebugId(r.id))) {
await restaurantRepo.deleteRestaurant(restaurant.id);
}
}
Future<bool> _hasExistingDebugData() async {
final visitRepo = _ref.read(visitRepositoryProvider);
final recommendationRepo = _ref.read(recommendationRepositoryProvider);
final restaurantRepo = _ref.read(restaurantRepositoryProvider);
final visits = await visitRepo.getAllVisitRecords();
final recos = await recommendationRepo.getAllRecommendationRecords();
final restaurants = await restaurantRepo.getAllRestaurants();
return visits.any((v) => _isDebugId(v.id)) ||
recos.any((r) => _isDebugId(r.id)) ||
restaurants.any((r) => _isDebugId(r.id));
}
List<_DebugSample> _buildDebugSamples() {
final today = DateTime.now();
final baseDay = DateTime(today.year, today.month, today.day);
DateTime atDayOffset(int daysAgo, {int hour = 12, int minute = 0}) {
return baseDay
.subtract(Duration(days: daysAgo))
.add(Duration(hours: hour, minutes: minute));
}
VisitRecord buildVisit({
required String id,
required String restaurantId,
required DateTime visitDate,
required bool isConfirmed,
}) {
return VisitRecord(
id: id,
restaurantId: restaurantId,
visitDate: visitDate,
isConfirmed: isConfirmed,
createdAt: visitDate,
);
}
RecommendationRecord buildRecommendation({
required String id,
required String restaurantId,
required DateTime recommendationDate,
}) {
return RecommendationRecord(
id: id,
restaurantId: restaurantId,
recommendationDate: recommendationDate,
visited: false,
createdAt: recommendationDate,
);
}
Restaurant buildRestaurant({
required String id,
required String name,
required String category,
required String subCategory,
required String roadAddress,
required String jibunAddress,
required double latitude,
required double longitude,
required String description,
required String phoneNumber,
required List<VisitRecord> visits,
}) {
final latestVisit = visits
.map((v) => v.visitDate)
.reduce((a, b) => a.isAfter(b) ? a : b);
return Restaurant(
id: id,
name: name,
category: category,
subCategory: subCategory,
description: description,
phoneNumber: phoneNumber,
roadAddress: roadAddress,
jibunAddress: jibunAddress,
latitude: latitude,
longitude: longitude,
lastVisitDate: latestVisit,
source: DataSource.PRESET,
createdAt: baseDay,
updatedAt: baseDay,
naverPlaceId: null,
naverUrl: null,
businessHours: null,
lastVisited: latestVisit,
visitCount: visits.length,
needsAddressVerification: false,
);
}
final bistroId = _withPrefix('bistro');
final sushiId = _withPrefix('sushi');
final coffeeId = _withPrefix('coffee');
final bistroVisits = [
buildVisit(
id: _withPrefix('visit-bistro-0'),
restaurantId: bistroId,
visitDate: atDayOffset(0, hour: 12, minute: 10),
isConfirmed: false,
),
buildVisit(
id: _withPrefix('visit-bistro-1'),
restaurantId: bistroId,
visitDate: atDayOffset(2, hour: 19, minute: 0),
isConfirmed: true,
),
buildVisit(
id: _withPrefix('visit-bistro-2'),
restaurantId: bistroId,
visitDate: atDayOffset(5, hour: 13, minute: 15),
isConfirmed: true,
),
];
final sushiVisits = [
buildVisit(
id: _withPrefix('visit-sushi-0'),
restaurantId: sushiId,
visitDate: atDayOffset(1, hour: 12, minute: 40),
isConfirmed: true,
),
buildVisit(
id: _withPrefix('visit-sushi-1'),
restaurantId: sushiId,
visitDate: atDayOffset(3, hour: 18, minute: 30),
isConfirmed: false,
),
buildVisit(
id: _withPrefix('visit-sushi-2'),
restaurantId: sushiId,
visitDate: atDayOffset(6, hour: 20, minute: 10),
isConfirmed: true,
),
];
final coffeeVisits = [
buildVisit(
id: _withPrefix('visit-coffee-0'),
restaurantId: coffeeId,
visitDate: atDayOffset(2, hour: 9, minute: 30),
isConfirmed: true,
),
buildVisit(
id: _withPrefix('visit-coffee-1'),
restaurantId: coffeeId,
visitDate: atDayOffset(4, hour: 15, minute: 15),
isConfirmed: true,
),
buildVisit(
id: _withPrefix('visit-coffee-2'),
restaurantId: coffeeId,
visitDate: atDayOffset(7, hour: 11, minute: 50),
isConfirmed: true,
),
];
final samples = <_DebugSample>[
_DebugSample(
restaurant: buildRestaurant(
id: bistroId,
name: 'Debug Bistro',
category: 'Fusion',
subCategory: 'Brunch',
description:
'Sample data to preview the record and statistics experience.',
phoneNumber: '02-100-0001',
roadAddress: '서울 테스트로 12',
jibunAddress: '서울 테스트동 12-1',
latitude: 37.5665,
longitude: 126.9780,
visits: bistroVisits,
),
visits: bistroVisits,
recommendations: [
buildRecommendation(
id: _withPrefix('reco-bistro-0'),
restaurantId: bistroId,
recommendationDate: atDayOffset(1, hour: 11, minute: 20),
),
buildRecommendation(
id: _withPrefix('reco-bistro-1'),
restaurantId: bistroId,
recommendationDate: atDayOffset(4, hour: 18, minute: 40),
),
],
),
_DebugSample(
restaurant: buildRestaurant(
id: sushiId,
name: 'Sample Sushi Bar',
category: 'Japanese',
subCategory: 'Sushi',
description: 'Rotating omakase picks to mimic real visit timelines.',
phoneNumber: '02-200-0002',
roadAddress: '서울 샘플로 21',
jibunAddress: '서울 샘플동 21-3',
latitude: 37.5559,
longitude: 126.9363,
visits: sushiVisits,
),
visits: sushiVisits,
recommendations: [
buildRecommendation(
id: _withPrefix('reco-sushi-0'),
restaurantId: sushiId,
recommendationDate: atDayOffset(3, hour: 12, minute: 0),
),
buildRecommendation(
id: _withPrefix('reco-sushi-1'),
restaurantId: sushiId,
recommendationDate: atDayOffset(7, hour: 19, minute: 10),
),
],
),
_DebugSample(
restaurant: buildRestaurant(
id: coffeeId,
name: 'Test Coffee Lab',
category: 'Cafe',
subCategory: 'Dessert',
description: 'Morning cafe stops added so charts render immediately.',
phoneNumber: '02-300-0003',
roadAddress: '서울 예제길 5',
jibunAddress: '서울 예제동 5-2',
latitude: 37.5412,
longitude: 126.986,
visits: coffeeVisits,
),
visits: coffeeVisits,
recommendations: [
buildRecommendation(
id: _withPrefix('reco-coffee-0'),
restaurantId: coffeeId,
recommendationDate: atDayOffset(0, hour: 8, minute: 50),
),
buildRecommendation(
id: _withPrefix('reco-coffee-1'),
restaurantId: coffeeId,
recommendationDate: atDayOffset(5, hour: 16, minute: 30),
),
],
),
];
return samples;
}
bool _isDebugId(String id) => id.startsWith(_idPrefix);
String _withPrefix(String rawId) => '$_idPrefix$rawId';
}
final debugTestDataNotifierProvider =
StateNotifierProvider<DebugTestDataNotifier, DebugTestDataState>((ref) {
return DebugTestDataNotifier(ref);
});
class _DebugSample {
final Restaurant restaurant;
final List<VisitRecord> visits;
final List<RecommendationRecord> recommendations;
_DebugSample({
required this.restaurant,
required this.visits,
required this.recommendations,
});
}