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:
@@ -32,6 +32,7 @@ Never commit API secrets. Instead, create `lib/core/constants/api_keys.dart` loc
|
|||||||
- 기본 응답은 한국어로 작성하고, 코드/로그/명령어는 원문을 유지합니다.
|
- 기본 응답은 한국어로 작성하고, 코드/로그/명령어는 원문을 유지합니다.
|
||||||
- Business logic, identifiers, and UI strings remain in English, but 주석과 문서 설명은 가능한 한 한국어로 작성하고 처음에는 해당 영어 용어를 괄호로 병기합니다.
|
- Business logic, identifiers, and UI strings remain in English, but 주석과 문서 설명은 가능한 한 한국어로 작성하고 처음에는 해당 영어 용어를 괄호로 병기합니다.
|
||||||
- Git push 보고나 작업 완료 보고 역시 한국어로 작성합니다.
|
- Git push 보고나 작업 완료 보고 역시 한국어로 작성합니다.
|
||||||
|
- 코드 주석, 커밋/PR/작업 요약 코멘트도 한국어로 작성하고 필요한 경우 영어 용어만 병기합니다.
|
||||||
|
|
||||||
## Validation & Quality Checks
|
## Validation & Quality Checks
|
||||||
- Run `dart format --set-exit-if-changed .` before finishing a task to ensure formatting stays consistent.
|
- Run `dart format --set-exit-if-changed .` before finishing a task to ensure formatting stays consistent.
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class _MockInterstitialAdDialogState extends State<_MockInterstitialAdDialog> {
|
|||||||
|
|
||||||
late Timer _timer;
|
late Timer _timer;
|
||||||
int _elapsedSeconds = 0;
|
int _elapsedSeconds = 0;
|
||||||
|
bool _completed = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -37,8 +38,10 @@ class _MockInterstitialAdDialogState extends State<_MockInterstitialAdDialog> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_elapsedSeconds++;
|
_elapsedSeconds++;
|
||||||
});
|
});
|
||||||
if (_elapsedSeconds >= _adDurationSeconds) {
|
if (_elapsedSeconds >= _adDurationSeconds && !_completed) {
|
||||||
|
_completed = true;
|
||||||
_timer.cancel();
|
_timer.cancel();
|
||||||
|
Navigator.of(context).pop(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -99,26 +102,13 @@ class _MockInterstitialAdDialogState extends State<_MockInterstitialAdDialog> {
|
|||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Text(
|
Text(
|
||||||
_canClose
|
_canClose
|
||||||
? '이제 닫을 수 있어요.'
|
? '광고가 완료되었어요. 자동으로 계속합니다.'
|
||||||
: '남은 시간: ${_adDurationSeconds - _elapsedSeconds}초',
|
: '남은 시간: ${_adDurationSeconds - _elapsedSeconds}초',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: isDark ? Colors.white70 : Colors.black54,
|
color: isDark ? Colors.white70 : Colors.black54,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 12),
|
||||||
FilledButton(
|
|
||||||
onPressed: _canClose
|
|
||||||
? () {
|
|
||||||
Navigator.of(context).pop(true);
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
style: FilledButton.styleFrom(
|
|
||||||
minimumSize: const Size.fromHeight(48),
|
|
||||||
backgroundColor: Colors.deepPurple,
|
|
||||||
),
|
|
||||||
child: const Text('추천 계속 보기'),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop(false);
|
Navigator.of(context).pop(false);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:table_calendar/table_calendar.dart';
|
import 'package:table_calendar/table_calendar.dart';
|
||||||
@@ -6,10 +7,12 @@ import '../../../core/constants/app_typography.dart';
|
|||||||
import '../../../domain/entities/recommendation_record.dart';
|
import '../../../domain/entities/recommendation_record.dart';
|
||||||
import '../../../domain/entities/visit_record.dart';
|
import '../../../domain/entities/visit_record.dart';
|
||||||
import '../../providers/recommendation_provider.dart';
|
import '../../providers/recommendation_provider.dart';
|
||||||
|
import '../../providers/debug_test_data_provider.dart';
|
||||||
import '../../providers/visit_provider.dart';
|
import '../../providers/visit_provider.dart';
|
||||||
import 'widgets/visit_record_card.dart';
|
import 'widgets/visit_record_card.dart';
|
||||||
import 'widgets/recommendation_record_card.dart';
|
import 'widgets/recommendation_record_card.dart';
|
||||||
import 'widgets/visit_statistics.dart';
|
import 'widgets/visit_statistics.dart';
|
||||||
|
import 'widgets/debug_test_data_banner.dart';
|
||||||
|
|
||||||
class CalendarScreen extends ConsumerStatefulWidget {
|
class CalendarScreen extends ConsumerStatefulWidget {
|
||||||
const CalendarScreen({super.key});
|
const CalendarScreen({super.key});
|
||||||
@@ -32,6 +35,11 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen>
|
|||||||
_selectedDay = DateTime.now();
|
_selectedDay = DateTime.now();
|
||||||
_focusedDay = DateTime.now();
|
_focusedDay = DateTime.now();
|
||||||
_tabController = TabController(length: 2, vsync: this);
|
_tabController = TabController(length: 2, vsync: this);
|
||||||
|
if (kDebugMode) {
|
||||||
|
Future.microtask(
|
||||||
|
() => ref.read(debugTestDataNotifierProvider.notifier).initialize(),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -100,6 +108,10 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen>
|
|||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
|
if (kDebugMode)
|
||||||
|
const DebugTestDataBanner(
|
||||||
|
margin: EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||||
|
),
|
||||||
// 캘린더
|
// 캘린더
|
||||||
Card(
|
Card(
|
||||||
margin: const EdgeInsets.all(16),
|
margin: const EdgeInsets.all(16),
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:lunchpick/core/constants/app_colors.dart';
|
||||||
|
import 'package:lunchpick/core/constants/app_typography.dart';
|
||||||
|
import 'package:lunchpick/presentation/providers/debug_test_data_provider.dart';
|
||||||
|
|
||||||
|
class DebugTestDataBanner extends ConsumerWidget {
|
||||||
|
final EdgeInsetsGeometry? margin;
|
||||||
|
|
||||||
|
const DebugTestDataBanner({super.key, this.margin});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
if (!kDebugMode) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
final state = ref.watch(debugTestDataNotifierProvider);
|
||||||
|
final notifier = ref.read(debugTestDataNotifierProvider.notifier);
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
margin: margin ?? const EdgeInsets.all(16),
|
||||||
|
color: isDark ? AppColors.darkSurface : AppColors.lightSurface,
|
||||||
|
elevation: 1,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.science_outlined,
|
||||||
|
color: AppColors.lightPrimary,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'테스트 데이터 미리보기',
|
||||||
|
style: AppTypography.body1(
|
||||||
|
isDark,
|
||||||
|
).copyWith(fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
if (state.isProcessing)
|
||||||
|
const SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Switch.adaptive(
|
||||||
|
value: state.isEnabled,
|
||||||
|
onChanged: state.isProcessing
|
||||||
|
? null
|
||||||
|
: (value) async {
|
||||||
|
if (value) {
|
||||||
|
await notifier.enableTestData();
|
||||||
|
} else {
|
||||||
|
await notifier.disableTestData();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
activeColor: AppColors.lightPrimary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
state.isEnabled
|
||||||
|
? '디버그 빌드에서만 적용됩니다. 기록/통계 UI를 테스트용 데이터로 확인하세요.'
|
||||||
|
: '디버그 빌드에서만 사용 가능합니다. 스위치를 켜면 추천·방문 기록이 자동으로 채워집니다.',
|
||||||
|
style: AppTypography.caption(isDark),
|
||||||
|
),
|
||||||
|
if (state.errorMessage != null) ...[
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Text(
|
||||||
|
state.errorMessage!,
|
||||||
|
style: AppTypography.caption(isDark).copyWith(
|
||||||
|
color: AppColors.lightError,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:lunchpick/core/constants/app_colors.dart';
|
import 'package:lunchpick/core/constants/app_colors.dart';
|
||||||
import 'package:lunchpick/core/constants/app_typography.dart';
|
import 'package:lunchpick/core/constants/app_typography.dart';
|
||||||
import 'package:lunchpick/presentation/providers/visit_provider.dart';
|
import 'package:lunchpick/presentation/providers/visit_provider.dart';
|
||||||
import 'package:lunchpick/presentation/providers/restaurant_provider.dart';
|
import 'package:lunchpick/presentation/providers/restaurant_provider.dart';
|
||||||
|
import 'package:lunchpick/presentation/pages/calendar/widgets/debug_test_data_banner.dart';
|
||||||
|
|
||||||
class VisitStatistics extends ConsumerWidget {
|
class VisitStatistics extends ConsumerWidget {
|
||||||
final DateTime selectedMonth;
|
final DateTime selectedMonth;
|
||||||
@@ -32,6 +34,10 @@ class VisitStatistics extends ConsumerWidget {
|
|||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
|
if (kDebugMode) ...[
|
||||||
|
const DebugTestDataBanner(margin: EdgeInsets.zero),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
],
|
||||||
// 이번 달 통계
|
// 이번 달 통계
|
||||||
_buildMonthlyStats(monthlyStatsAsync, isDark),
|
_buildMonthlyStats(monthlyStatsAsync, isDark),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
@@ -130,7 +136,7 @@ class VisitStatistics extends ConsumerWidget {
|
|||||||
: stats.values.reduce((a, b) => a > b ? a : b);
|
: stats.values.reduce((a, b) => a > b ? a : b);
|
||||||
|
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: 120,
|
height: 140,
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:lunchpick/core/constants/app_colors.dart';
|
import 'package:lunchpick/core/constants/app_colors.dart';
|
||||||
@@ -84,6 +85,9 @@ class _ShareScreenState extends ConsumerState<ShareScreen> {
|
|||||||
List<ShareDevice>? _nearbyDevices;
|
List<ShareDevice>? _nearbyDevices;
|
||||||
StreamSubscription<String>? _dataSubscription;
|
StreamSubscription<String>? _dataSubscription;
|
||||||
final _uuid = const Uuid();
|
final _uuid = const Uuid();
|
||||||
|
bool _debugPreviewEnabled = false;
|
||||||
|
bool _debugPreviewProcessing = false;
|
||||||
|
Timer? _debugPreviewTimer;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -98,6 +102,7 @@ class _ShareScreenState extends ConsumerState<ShareScreen> {
|
|||||||
void dispose() {
|
void dispose() {
|
||||||
_dataSubscription?.cancel();
|
_dataSubscription?.cancel();
|
||||||
ref.read(bluetoothServiceProvider).stopListening();
|
ref.read(bluetoothServiceProvider).stopListening();
|
||||||
|
_debugPreviewTimer?.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,6 +131,10 @@ class _ShareScreenState extends ConsumerState<ShareScreen> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
|
if (kDebugMode) ...[
|
||||||
|
_buildDebugToggle(isDark),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
_ShareCard(
|
_ShareCard(
|
||||||
isDark: isDark,
|
isDark: isDark,
|
||||||
icon: Icons.upload_rounded,
|
icon: Icons.upload_rounded,
|
||||||
@@ -294,6 +303,23 @@ class _ShareScreenState extends ConsumerState<ShareScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _generateShareCode() async {
|
Future<void> _generateShareCode() async {
|
||||||
|
final adWatched = await ref
|
||||||
|
.read(adServiceProvider)
|
||||||
|
.showInterstitialAd(context);
|
||||||
|
if (!mounted) return;
|
||||||
|
if (!adWatched) {
|
||||||
|
_showErrorSnackBar('광고를 끝까지 시청해야 공유 코드를 생성할 수 있어요.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kDebugMode && _debugPreviewEnabled) {
|
||||||
|
setState(() {
|
||||||
|
_shareCode = _shareCode ?? _buildDebugShareCode();
|
||||||
|
});
|
||||||
|
_showSuccessSnackBar('디버그 공유 코드가 준비되었습니다.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final hasPermission =
|
final hasPermission =
|
||||||
await PermissionService.checkAndRequestBluetoothPermission();
|
await PermissionService.checkAndRequestBluetoothPermission();
|
||||||
if (!hasPermission) {
|
if (!hasPermission) {
|
||||||
@@ -302,15 +328,6 @@ class _ShareScreenState extends ConsumerState<ShareScreen> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final adService = ref.read(adServiceProvider);
|
|
||||||
if (!mounted) return;
|
|
||||||
final adWatched = await adService.showInterstitialAd(context);
|
|
||||||
if (!mounted) return;
|
|
||||||
if (!adWatched) {
|
|
||||||
_showErrorSnackBar('광고를 끝까지 시청해야 공유 코드를 생성할 수 있어요.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final random = Random();
|
final random = Random();
|
||||||
final code = List.generate(6, (_) => random.nextInt(10)).join();
|
final code = List.generate(6, (_) => random.nextInt(10)).join();
|
||||||
|
|
||||||
@@ -328,6 +345,15 @@ class _ShareScreenState extends ConsumerState<ShareScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _scanDevices() async {
|
Future<void> _scanDevices() async {
|
||||||
|
if (kDebugMode && _debugPreviewEnabled) {
|
||||||
|
setState(() {
|
||||||
|
_isScanning = true;
|
||||||
|
_nearbyDevices = _buildDebugDevices();
|
||||||
|
});
|
||||||
|
_scheduleDebugReceive();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final hasPermission =
|
final hasPermission =
|
||||||
await PermissionService.checkAndRequestBluetoothPermission();
|
await PermissionService.checkAndRequestBluetoothPermission();
|
||||||
if (!hasPermission) {
|
if (!hasPermission) {
|
||||||
@@ -359,6 +385,28 @@ class _ShareScreenState extends ConsumerState<ShareScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _sendList(String targetCode) async {
|
Future<void> _sendList(String targetCode) async {
|
||||||
|
final adWatched = await ref
|
||||||
|
.read(adServiceProvider)
|
||||||
|
.showInterstitialAd(context);
|
||||||
|
if (!mounted) return;
|
||||||
|
if (!adWatched) {
|
||||||
|
_showErrorSnackBar('광고를 끝까지 시청해야 리스트를 전송할 수 있어요.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kDebugMode && _debugPreviewEnabled) {
|
||||||
|
_showLoadingDialog('리스트 전송 중...');
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 700));
|
||||||
|
if (!mounted) return;
|
||||||
|
Navigator.pop(context);
|
||||||
|
_showSuccessSnackBar('디버그 전송 완료! (실제 전송 없음)');
|
||||||
|
setState(() {
|
||||||
|
_isScanning = false;
|
||||||
|
_nearbyDevices = null;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final restaurants = await ref.read(restaurantListProvider.future);
|
final restaurants = await ref.read(restaurantListProvider.future);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
@@ -381,15 +429,20 @@ class _ShareScreenState extends ConsumerState<ShareScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _handleIncomingData(String payload) async {
|
Future<void> _handleIncomingData(
|
||||||
|
String payload, {
|
||||||
|
bool skipAd = false,
|
||||||
|
}) async {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final adWatched = await ref
|
if (!skipAd) {
|
||||||
.read(adServiceProvider)
|
final adWatched = await ref
|
||||||
.showInterstitialAd(context);
|
.read(adServiceProvider)
|
||||||
if (!mounted) return;
|
.showInterstitialAd(context);
|
||||||
if (!adWatched) {
|
if (!mounted) return;
|
||||||
_showErrorSnackBar('광고를 시청해야 리스트를 받을 수 있어요.');
|
if (!adWatched) {
|
||||||
return;
|
_showErrorSnackBar('광고를 시청해야 리스트를 받을 수 있어요.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -520,4 +573,229 @@ class _ShareScreenState extends ConsumerState<ShareScreen> {
|
|||||||
SnackBar(content: Text(message), backgroundColor: AppColors.lightError),
|
SnackBar(content: Text(message), backgroundColor: AppColors.lightError),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildDebugToggle(bool isDark) {
|
||||||
|
return Card(
|
||||||
|
color: isDark ? AppColors.darkSurface : AppColors.lightSurface,
|
||||||
|
elevation: 1,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(14),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.lightPrimary.withOpacity(0.1),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.science_outlined,
|
||||||
|
color: AppColors.lightPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'테스트 토글 (디버그 전용)',
|
||||||
|
style: AppTypography.body1(
|
||||||
|
isDark,
|
||||||
|
).copyWith(fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
_debugPreviewEnabled
|
||||||
|
? '샘플 코드·기기와 수신 데이터가 자동으로 표시됩니다.'
|
||||||
|
: '토글을 켜면 광고/권한 없이 공유 UI를 미리 볼 수 있습니다.',
|
||||||
|
style: AppTypography.caption(isDark),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
if (_debugPreviewProcessing)
|
||||||
|
const SizedBox(
|
||||||
|
width: 22,
|
||||||
|
height: 22,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Switch.adaptive(
|
||||||
|
value: _debugPreviewEnabled,
|
||||||
|
onChanged: _debugPreviewProcessing
|
||||||
|
? null
|
||||||
|
: (value) {
|
||||||
|
_toggleDebugPreview(value);
|
||||||
|
},
|
||||||
|
activeColor: AppColors.lightPrimary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _toggleDebugPreview(bool enabled) async {
|
||||||
|
if (_debugPreviewProcessing) return;
|
||||||
|
setState(() {
|
||||||
|
_debugPreviewProcessing = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (enabled) {
|
||||||
|
await _startDebugPreviewFlow();
|
||||||
|
} else {
|
||||||
|
_stopDebugPreviewFlow();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_debugPreviewEnabled = enabled;
|
||||||
|
_debugPreviewProcessing = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _startDebugPreviewFlow() async {
|
||||||
|
_debugPreviewTimer?.cancel();
|
||||||
|
final code = _buildDebugShareCode();
|
||||||
|
setState(() {
|
||||||
|
_shareCode = code;
|
||||||
|
_isScanning = true;
|
||||||
|
_nearbyDevices = _buildDebugDevices();
|
||||||
|
});
|
||||||
|
_scheduleDebugReceive();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _stopDebugPreviewFlow() {
|
||||||
|
_debugPreviewTimer?.cancel();
|
||||||
|
setState(() {
|
||||||
|
_shareCode = null;
|
||||||
|
_isScanning = false;
|
||||||
|
_nearbyDevices = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _scheduleDebugReceive() {
|
||||||
|
_debugPreviewTimer?.cancel();
|
||||||
|
_debugPreviewTimer = Timer(const Duration(seconds: 1), () {
|
||||||
|
if (!mounted || !_debugPreviewEnabled) return;
|
||||||
|
final payload = _buildDebugPayload();
|
||||||
|
_handleIncomingData(payload, skipAd: true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
String _buildDebugShareCode() => 'DBG${Random().nextInt(900000) + 100000}';
|
||||||
|
|
||||||
|
List<ShareDevice> _buildDebugDevices() {
|
||||||
|
final now = DateTime.now();
|
||||||
|
return [
|
||||||
|
ShareDevice(code: 'DBG-ALPHA', deviceId: 'LP-DEBUG-1', discoveredAt: now),
|
||||||
|
ShareDevice(
|
||||||
|
code: 'DBG-BETA',
|
||||||
|
deviceId: 'LP-DEBUG-2',
|
||||||
|
discoveredAt: now.subtract(const Duration(seconds: 10)),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
String _buildDebugPayload() {
|
||||||
|
final samples = _buildDebugRestaurants();
|
||||||
|
final list = samples
|
||||||
|
.map(
|
||||||
|
(restaurant) => {
|
||||||
|
'id': restaurant.id,
|
||||||
|
'name': restaurant.name,
|
||||||
|
'category': restaurant.category,
|
||||||
|
'subCategory': restaurant.subCategory,
|
||||||
|
'description': restaurant.description,
|
||||||
|
'phoneNumber': restaurant.phoneNumber,
|
||||||
|
'roadAddress': restaurant.roadAddress,
|
||||||
|
'jibunAddress': restaurant.jibunAddress,
|
||||||
|
'latitude': restaurant.latitude,
|
||||||
|
'longitude': restaurant.longitude,
|
||||||
|
'lastVisitDate': restaurant.lastVisitDate?.toIso8601String(),
|
||||||
|
'source': restaurant.source.name,
|
||||||
|
'createdAt': restaurant.createdAt.toIso8601String(),
|
||||||
|
'updatedAt': restaurant.updatedAt.toIso8601String(),
|
||||||
|
'naverPlaceId': restaurant.naverPlaceId,
|
||||||
|
'naverUrl': restaurant.naverUrl,
|
||||||
|
'businessHours': restaurant.businessHours,
|
||||||
|
'lastVisited': restaurant.lastVisited?.toIso8601String(),
|
||||||
|
'visitCount': restaurant.visitCount,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
return jsonEncode(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Restaurant> _buildDebugRestaurants() {
|
||||||
|
final now = DateTime.now();
|
||||||
|
return [
|
||||||
|
Restaurant(
|
||||||
|
id: 'debug-share-ramen',
|
||||||
|
name: '디버그 라멘바',
|
||||||
|
category: 'Japanese',
|
||||||
|
subCategory: 'Ramen',
|
||||||
|
description: '테스트용 라멘 바. 실제 전송 없이 미리보기 용도입니다.',
|
||||||
|
phoneNumber: '02-111-1111',
|
||||||
|
roadAddress: '서울 특별시 테스트로 1',
|
||||||
|
jibunAddress: '서울 테스트동 1-1',
|
||||||
|
latitude: 37.566,
|
||||||
|
longitude: 126.9784,
|
||||||
|
lastVisitDate: now.subtract(const Duration(days: 2)),
|
||||||
|
source: DataSource.PRESET,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
naverPlaceId: null,
|
||||||
|
naverUrl: null,
|
||||||
|
businessHours: '11:00 - 21:00',
|
||||||
|
lastVisited: now.subtract(const Duration(days: 2)),
|
||||||
|
visitCount: 3,
|
||||||
|
),
|
||||||
|
Restaurant(
|
||||||
|
id: 'debug-share-burger',
|
||||||
|
name: '샘플 버거샵',
|
||||||
|
category: 'Fastfood',
|
||||||
|
subCategory: 'Burger',
|
||||||
|
description: '광고·권한 없이 교환 흐름을 확인하는 샘플 버거 가게.',
|
||||||
|
phoneNumber: '02-222-2222',
|
||||||
|
roadAddress: '서울 특별시 디버그길 22',
|
||||||
|
jibunAddress: '서울 디버그동 22-2',
|
||||||
|
latitude: 37.57,
|
||||||
|
longitude: 126.982,
|
||||||
|
lastVisitDate: now.subtract(const Duration(days: 5)),
|
||||||
|
source: DataSource.PRESET,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
naverPlaceId: null,
|
||||||
|
naverUrl: null,
|
||||||
|
businessHours: '10:00 - 23:00',
|
||||||
|
lastVisited: now.subtract(const Duration(days: 5)),
|
||||||
|
visitCount: 1,
|
||||||
|
),
|
||||||
|
Restaurant(
|
||||||
|
id: 'debug-share-brunch',
|
||||||
|
name: '프리뷰 브런치 카페',
|
||||||
|
category: 'Cafe',
|
||||||
|
subCategory: 'Brunch',
|
||||||
|
description: '리스트 공유 수신 UI를 확인하기 위한 브런치 카페 샘플.',
|
||||||
|
phoneNumber: '02-333-3333',
|
||||||
|
roadAddress: '서울 특별시 미리보기로 33',
|
||||||
|
jibunAddress: '서울 미리보기동 33-3',
|
||||||
|
latitude: 37.561,
|
||||||
|
longitude: 126.99,
|
||||||
|
lastVisitDate: now.subtract(const Duration(days: 1)),
|
||||||
|
source: DataSource.PRESET,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
naverPlaceId: null,
|
||||||
|
naverUrl: null,
|
||||||
|
businessHours: '09:00 - 18:00',
|
||||||
|
lastVisited: now.subtract(const Duration(days: 1)),
|
||||||
|
visitCount: 4,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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