import 'dart:async'; import 'dart:convert'; import 'dart:math'; 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:uuid/uuid.dart'; class ShareScreen extends ConsumerStatefulWidget { const ShareScreen({super.key}); @override ConsumerState createState() => _ShareScreenState(); } class _ShareScreenState extends ConsumerState { String? _shareCode; bool _isScanning = false; List? _nearbyDevices; StreamSubscription? _dataSubscription; final _uuid = const Uuid(); @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(); 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: Column( children: [ // 공유받기 섹션 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: AppColors.lightPrimary.withOpacity(0.1), shape: BoxShape.circle, ), child: const Icon( Icons.download_rounded, size: 48, color: AppColors.lightPrimary, ), ), const SizedBox(height: 16), Text('리스트 공유받기', style: AppTypography.heading2(isDark)), const SizedBox(height: 8), Text( '다른 사람의 맛집 리스트를 받아보세요', style: AppTypography.body2(isDark), textAlign: TextAlign.center, ), const SizedBox(height: 20), 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.qr_code), 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), ), ), ), ], ), ), ), const SizedBox(height: 16), // 공유하기 섹션 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: AppColors.lightSecondary.withOpacity(0.1), shape: BoxShape.circle, ), child: const Icon( Icons.upload_rounded, size: 48, color: AppColors.lightSecondary, ), ), const SizedBox(height: 16), Text('내 리스트 공유하기', style: AppTypography.heading2(isDark)), const SizedBox(height: 8), Text( '내 맛집 리스트를 다른 사람과 공유하세요', style: AppTypography.body2(isDark), textAlign: TextAlign.center, ), const SizedBox(height: 20), 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 _generateShareCode() async { final adService = ref.read(adServiceProvider); final adWatched = await adService.showInterstitialAd(context); if (!mounted) return; if (!adWatched) { _showErrorSnackBar('광고를 끝까지 시청해야 공유 코드를 생성할 수 있어요.'); return; } final random = Random(); final code = List.generate(6, (_) => random.nextInt(10)).join(); setState(() { _shareCode = code; }); await ref.read(bluetoothServiceProvider).startListening(code); _showSuccessSnackBar('공유 코드가 생성되었습니다.'); } Future _scanDevices() async { 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 _sendList(String targetCode) async { 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 _handleIncomingData(String payload) async { if (!mounted) return; 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 _parseReceivedData(String data) { final jsonList = jsonDecode(data) as List; return jsonList .map((item) => _restaurantFromJson(item as Map)) .toList(); } Restaurant _restaurantFromJson(Map 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 _mergeRestaurantList(List receivedList) async { final currentList = await ref.read(restaurantListProvider.future); final notifier = ref.read(restaurantNotifierProvider.notifier); final newRestaurants = []; 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), ); } }