805 lines
26 KiB
Dart
805 lines
26 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'dart:math';
|
|
|
|
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/core/services/permission_service.dart';
|
|
import 'package:lunchpick/domain/entities/restaurant.dart';
|
|
import 'package:lunchpick/domain/entities/share_device.dart';
|
|
import 'package:lunchpick/presentation/providers/ad_provider.dart';
|
|
import 'package:lunchpick/presentation/providers/bluetooth_provider.dart';
|
|
import 'package:lunchpick/presentation/providers/restaurant_provider.dart';
|
|
import 'package:lunchpick/presentation/widgets/native_ad_placeholder.dart';
|
|
import 'package:uuid/uuid.dart';
|
|
|
|
class ShareScreen extends ConsumerStatefulWidget {
|
|
const ShareScreen({super.key});
|
|
|
|
@override
|
|
ConsumerState<ShareScreen> createState() => _ShareScreenState();
|
|
}
|
|
|
|
class _ShareCard extends StatelessWidget {
|
|
final bool isDark;
|
|
final IconData icon;
|
|
final Color iconColor;
|
|
final Color iconBgColor;
|
|
final String title;
|
|
final String subtitle;
|
|
final Widget child;
|
|
|
|
const _ShareCard({
|
|
required this.isDark,
|
|
required this.icon,
|
|
required this.iconColor,
|
|
required this.iconBgColor,
|
|
required this.title,
|
|
required this.subtitle,
|
|
required this.child,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SizedBox(
|
|
width: double.infinity,
|
|
child: Card(
|
|
color: isDark ? AppColors.darkSurface : AppColors.lightSurface,
|
|
elevation: 2,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(24),
|
|
child: Column(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: iconBgColor,
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Icon(icon, size: 48, color: iconColor),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(title, style: AppTypography.heading2(isDark)),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
subtitle,
|
|
style: AppTypography.body2(isDark),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
const SizedBox(height: 20),
|
|
child,
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _ShareScreenState extends ConsumerState<ShareScreen> {
|
|
String? _shareCode;
|
|
bool _isScanning = false;
|
|
List<ShareDevice>? _nearbyDevices;
|
|
StreamSubscription<String>? _dataSubscription;
|
|
final _uuid = const Uuid();
|
|
bool _debugPreviewEnabled = false;
|
|
bool _debugPreviewProcessing = false;
|
|
Timer? _debugPreviewTimer;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
final bluetoothService = ref.read(bluetoothServiceProvider);
|
|
_dataSubscription = bluetoothService.onDataReceived.listen((payload) {
|
|
_handleIncomingData(payload);
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_dataSubscription?.cancel();
|
|
ref.read(bluetoothServiceProvider).stopListening();
|
|
_debugPreviewTimer?.cancel();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
|
|
|
return Scaffold(
|
|
backgroundColor: isDark
|
|
? AppColors.darkBackground
|
|
: AppColors.lightBackground,
|
|
appBar: AppBar(
|
|
title: const Text('리스트 공유'),
|
|
backgroundColor: isDark
|
|
? AppColors.darkPrimary
|
|
: AppColors.lightPrimary,
|
|
foregroundColor: Colors.white,
|
|
elevation: 0,
|
|
),
|
|
body: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Center(
|
|
child: ConstrainedBox(
|
|
constraints: const BoxConstraints(maxWidth: 520),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
if (kDebugMode) ...[
|
|
_buildDebugToggle(isDark),
|
|
const SizedBox(height: 16),
|
|
],
|
|
_ShareCard(
|
|
isDark: isDark,
|
|
icon: Icons.upload_rounded,
|
|
iconColor: AppColors.lightSecondary,
|
|
iconBgColor: AppColors.lightSecondary.withOpacity(0.1),
|
|
title: '내 리스트 공유하기',
|
|
subtitle: '내 맛집 리스트를 다른 사람과 공유하세요',
|
|
child: _buildSendSection(isDark),
|
|
),
|
|
const SizedBox(height: 16),
|
|
const NativeAdPlaceholder(),
|
|
const SizedBox(height: 16),
|
|
_ShareCard(
|
|
isDark: isDark,
|
|
icon: Icons.download_rounded,
|
|
iconColor: AppColors.lightPrimary,
|
|
iconBgColor: AppColors.lightPrimary.withOpacity(0.1),
|
|
title: '리스트 공유받기',
|
|
subtitle: '다른 사람의 맛집 리스트를 받아보세요',
|
|
child: _buildReceiveSection(isDark),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildReceiveSection(bool isDark) {
|
|
return Column(
|
|
children: [
|
|
if (_shareCode != null) ...[
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
|
decoration: BoxDecoration(
|
|
color: AppColors.lightPrimary.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(
|
|
color: AppColors.lightPrimary.withOpacity(0.3),
|
|
width: 2,
|
|
),
|
|
),
|
|
child: Text(
|
|
_shareCode!,
|
|
style: const TextStyle(
|
|
fontSize: 36,
|
|
fontWeight: FontWeight.bold,
|
|
letterSpacing: 6,
|
|
color: AppColors.lightPrimary,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Text('이 코드를 상대방에게 알려주세요', style: AppTypography.caption(isDark)),
|
|
const SizedBox(height: 16),
|
|
TextButton.icon(
|
|
onPressed: () {
|
|
setState(() {
|
|
_shareCode = null;
|
|
});
|
|
ref.read(bluetoothServiceProvider).stopListening();
|
|
},
|
|
icon: const Icon(Icons.close),
|
|
label: const Text('취소'),
|
|
style: TextButton.styleFrom(foregroundColor: AppColors.lightError),
|
|
),
|
|
] else
|
|
ElevatedButton.icon(
|
|
onPressed: () {
|
|
_generateShareCode();
|
|
},
|
|
icon: const Icon(Icons.bluetooth),
|
|
label: const Text('공유 코드 생성'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: AppColors.lightPrimary,
|
|
foregroundColor: Colors.white,
|
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildSendSection(bool isDark) {
|
|
return Column(
|
|
children: [
|
|
if (_isScanning && _nearbyDevices != null) ...[
|
|
Container(
|
|
constraints: const BoxConstraints(maxHeight: 220),
|
|
child: _nearbyDevices!.isEmpty
|
|
? Column(
|
|
children: [
|
|
const CircularProgressIndicator(
|
|
color: AppColors.lightSecondary,
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
'주변 기기를 검색 중...',
|
|
style: AppTypography.caption(isDark),
|
|
),
|
|
],
|
|
)
|
|
: ListView.builder(
|
|
itemCount: _nearbyDevices!.length,
|
|
shrinkWrap: true,
|
|
itemBuilder: (context, index) {
|
|
final device = _nearbyDevices![index];
|
|
return Card(
|
|
margin: const EdgeInsets.only(bottom: 8),
|
|
child: ListTile(
|
|
leading: const Icon(
|
|
Icons.phone_android,
|
|
color: AppColors.lightSecondary,
|
|
),
|
|
title: Text(
|
|
device.code,
|
|
style: AppTypography.body1(
|
|
isDark,
|
|
).copyWith(fontWeight: FontWeight.bold),
|
|
),
|
|
subtitle: Text('기기 ID: ${device.deviceId}'),
|
|
trailing: const Icon(
|
|
Icons.send,
|
|
color: AppColors.lightSecondary,
|
|
),
|
|
onTap: () {
|
|
_sendList(device.code);
|
|
},
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
TextButton.icon(
|
|
onPressed: () {
|
|
setState(() {
|
|
_isScanning = false;
|
|
_nearbyDevices = null;
|
|
});
|
|
},
|
|
icon: const Icon(Icons.stop),
|
|
label: const Text('스캔 중지'),
|
|
style: TextButton.styleFrom(foregroundColor: AppColors.lightError),
|
|
),
|
|
] else
|
|
ElevatedButton.icon(
|
|
onPressed: () {
|
|
_scanDevices();
|
|
},
|
|
icon: const Icon(Icons.radar),
|
|
label: const Text('주변 기기 스캔'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: AppColors.lightSecondary,
|
|
foregroundColor: Colors.white,
|
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
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 =
|
|
await PermissionService.checkAndRequestBluetoothPermission();
|
|
if (!hasPermission) {
|
|
if (!mounted) return;
|
|
_showErrorSnackBar('블루투스 권한을 허용해야 공유 코드를 생성할 수 있어요.');
|
|
return;
|
|
}
|
|
|
|
final random = Random();
|
|
final code = List.generate(6, (_) => random.nextInt(10)).join();
|
|
|
|
try {
|
|
await ref.read(bluetoothServiceProvider).startListening(code);
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_shareCode = code;
|
|
});
|
|
_showSuccessSnackBar('공유 코드가 생성되었습니다.');
|
|
} catch (_) {
|
|
if (!mounted) return;
|
|
_showErrorSnackBar('코드를 생성하지 못했습니다. 잠시 후 다시 시도해 주세요.');
|
|
}
|
|
}
|
|
|
|
Future<void> _scanDevices() async {
|
|
if (kDebugMode && _debugPreviewEnabled) {
|
|
setState(() {
|
|
_isScanning = true;
|
|
_nearbyDevices = _buildDebugDevices();
|
|
});
|
|
_scheduleDebugReceive();
|
|
return;
|
|
}
|
|
|
|
final hasPermission =
|
|
await PermissionService.checkAndRequestBluetoothPermission();
|
|
if (!hasPermission) {
|
|
if (!mounted) return;
|
|
_showErrorSnackBar('블루투스 권한이 필요합니다.');
|
|
return;
|
|
}
|
|
|
|
setState(() {
|
|
_isScanning = true;
|
|
_nearbyDevices = [];
|
|
});
|
|
|
|
try {
|
|
final devices = await ref
|
|
.read(bluetoothServiceProvider)
|
|
.scanNearbyDevices();
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_nearbyDevices = devices;
|
|
});
|
|
} catch (e) {
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_isScanning = false;
|
|
});
|
|
_showErrorSnackBar('스캔 중 오류가 발생했습니다.');
|
|
}
|
|
}
|
|
|
|
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);
|
|
if (!mounted) return;
|
|
|
|
_showLoadingDialog('리스트 전송 중...');
|
|
try {
|
|
await ref
|
|
.read(bluetoothServiceProvider)
|
|
.sendRestaurantList(targetCode, restaurants);
|
|
if (!mounted) return;
|
|
Navigator.pop(context);
|
|
_showSuccessSnackBar('리스트 전송 완료!');
|
|
setState(() {
|
|
_isScanning = false;
|
|
_nearbyDevices = null;
|
|
});
|
|
} catch (e) {
|
|
if (!mounted) return;
|
|
Navigator.pop(context);
|
|
_showErrorSnackBar('전송 실패: $e');
|
|
}
|
|
}
|
|
|
|
Future<void> _handleIncomingData(
|
|
String payload, {
|
|
bool skipAd = false,
|
|
}) async {
|
|
if (!mounted) return;
|
|
if (!skipAd) {
|
|
final adWatched = await ref
|
|
.read(adServiceProvider)
|
|
.showInterstitialAd(context);
|
|
if (!mounted) return;
|
|
if (!adWatched) {
|
|
_showErrorSnackBar('광고를 시청해야 리스트를 받을 수 있어요.');
|
|
return;
|
|
}
|
|
}
|
|
|
|
try {
|
|
final restaurants = _parseReceivedData(payload);
|
|
await _mergeRestaurantList(restaurants);
|
|
} catch (_) {
|
|
_showErrorSnackBar('전송된 데이터를 처리하는 데 실패했습니다.');
|
|
}
|
|
}
|
|
|
|
List<Restaurant> _parseReceivedData(String data) {
|
|
final jsonList = jsonDecode(data) as List<dynamic>;
|
|
return jsonList
|
|
.map((item) => _restaurantFromJson(item as Map<String, dynamic>))
|
|
.toList();
|
|
}
|
|
|
|
Restaurant _restaurantFromJson(Map<String, dynamic> json) {
|
|
return Restaurant(
|
|
id: json['id'] as String,
|
|
name: json['name'] as String,
|
|
category: json['category'] as String,
|
|
subCategory: json['subCategory'] as String,
|
|
description: json['description'] as String?,
|
|
phoneNumber: json['phoneNumber'] as String?,
|
|
roadAddress: json['roadAddress'] as String,
|
|
jibunAddress: json['jibunAddress'] as String,
|
|
latitude: (json['latitude'] as num).toDouble(),
|
|
longitude: (json['longitude'] as num).toDouble(),
|
|
lastVisitDate: json['lastVisitDate'] != null
|
|
? DateTime.parse(json['lastVisitDate'] as String)
|
|
: null,
|
|
source: DataSource.values.firstWhere(
|
|
(source) =>
|
|
source.name ==
|
|
(json['source'] as String? ?? DataSource.USER_INPUT.name),
|
|
orElse: () => DataSource.USER_INPUT,
|
|
),
|
|
createdAt: DateTime.parse(json['createdAt'] as String),
|
|
updatedAt: DateTime.parse(json['updatedAt'] as String),
|
|
naverPlaceId: json['naverPlaceId'] as String?,
|
|
naverUrl: json['naverUrl'] as String?,
|
|
businessHours: json['businessHours'] as String?,
|
|
lastVisited: json['lastVisited'] != null
|
|
? DateTime.parse(json['lastVisited'] as String)
|
|
: null,
|
|
visitCount: (json['visitCount'] as num?)?.toInt() ?? 0,
|
|
);
|
|
}
|
|
|
|
Future<void> _mergeRestaurantList(List<Restaurant> receivedList) async {
|
|
final currentList = await ref.read(restaurantListProvider.future);
|
|
final notifier = ref.read(restaurantNotifierProvider.notifier);
|
|
|
|
final newRestaurants = <Restaurant>[];
|
|
for (final restaurant in receivedList) {
|
|
final exists = currentList.any(
|
|
(existing) =>
|
|
existing.name == restaurant.name &&
|
|
existing.roadAddress == restaurant.roadAddress,
|
|
);
|
|
if (!exists) {
|
|
newRestaurants.add(
|
|
restaurant.copyWith(
|
|
id: _uuid.v4(),
|
|
createdAt: DateTime.now(),
|
|
updatedAt: DateTime.now(),
|
|
source: DataSource.USER_INPUT,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
for (final restaurant in newRestaurants) {
|
|
await notifier.addRestaurantDirect(restaurant);
|
|
}
|
|
|
|
if (!mounted) return;
|
|
if (newRestaurants.isEmpty) {
|
|
_showSuccessSnackBar('이미 등록된 맛집과 동일한 항목만 전송되었습니다.');
|
|
} else {
|
|
_showSuccessSnackBar('${newRestaurants.length}개의 새로운 맛집이 추가되었습니다!');
|
|
}
|
|
|
|
setState(() {
|
|
_shareCode = null;
|
|
});
|
|
ref.read(bluetoothServiceProvider).stopListening();
|
|
}
|
|
|
|
void _showLoadingDialog(String message) {
|
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
|
|
|
showDialog(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder: (context) => Dialog(
|
|
backgroundColor: isDark
|
|
? AppColors.darkSurface
|
|
: AppColors.lightSurface,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const CircularProgressIndicator(color: AppColors.lightPrimary),
|
|
const SizedBox(width: 20),
|
|
Flexible(
|
|
child: Text(message, style: AppTypography.body2(isDark)),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showSuccessSnackBar(String message) {
|
|
if (!mounted) return;
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text(message), backgroundColor: AppColors.lightPrimary),
|
|
);
|
|
}
|
|
|
|
void _showErrorSnackBar(String message) {
|
|
if (!mounted) return;
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
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,
|
|
),
|
|
];
|
|
}
|
|
}
|