- 기록/통계 탭에 디버그 토글 배너 추가 및 테스트 데이터 주입 로직 상태화\n- 리스트 공유 화면에 디버그 프리뷰 토글, 광고 관문, 디버그 전송 흐름 반영\n- 모의 전면 광고는 대기 시간 종료 시 자동 완료되도록 변경\n- AGENTS.md에 코멘트는 한국어로 작성 규칙 명시\n\n테스트: flutter analyze; flutter test
389 lines
12 KiB
Dart
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,
|
|
});
|
|
}
|