feat(debug): add preview toggles and auto-close ads
- 기록/통계 탭에 디버그 토글 배너 추가 및 테스트 데이터 주입 로직 상태화\n- 리스트 공유 화면에 디버그 프리뷰 토글, 광고 관문, 디버그 전송 흐름 반영\n- 모의 전면 광고는 대기 시간 종료 시 자동 완료되도록 변경\n- AGENTS.md에 코멘트는 한국어로 작성 규칙 명시\n\n테스트: flutter analyze; flutter test
This commit is contained in:
388
lib/presentation/providers/debug_test_data_provider.dart
Normal file
388
lib/presentation/providers/debug_test_data_provider.dart
Normal file
@@ -0,0 +1,388 @@
|
||||
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,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user