feat(app): add manual entry and sharing flows
This commit is contained in:
@@ -1,36 +1,48 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
|
||||
import '../../../core/constants/app_colors.dart';
|
||||
import '../../../core/constants/app_typography.dart';
|
||||
import '../../../domain/entities/weather_info.dart';
|
||||
import '../../../domain/entities/restaurant.dart';
|
||||
import '../../providers/restaurant_provider.dart';
|
||||
import '../../providers/weather_provider.dart';
|
||||
import '../../../domain/entities/weather_info.dart';
|
||||
import '../../providers/ad_provider.dart';
|
||||
import '../../providers/location_provider.dart';
|
||||
import '../../providers/notification_provider.dart';
|
||||
import '../../providers/recommendation_provider.dart';
|
||||
import '../../providers/restaurant_provider.dart';
|
||||
import '../../providers/settings_provider.dart'
|
||||
show notificationDelayMinutesProvider, notificationEnabledProvider;
|
||||
import '../../providers/visit_provider.dart';
|
||||
import '../../providers/weather_provider.dart';
|
||||
import 'widgets/recommendation_result_dialog.dart';
|
||||
|
||||
class RandomSelectionScreen extends ConsumerStatefulWidget {
|
||||
const RandomSelectionScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<RandomSelectionScreen> createState() => _RandomSelectionScreenState();
|
||||
ConsumerState<RandomSelectionScreen> createState() =>
|
||||
_RandomSelectionScreenState();
|
||||
}
|
||||
|
||||
class _RandomSelectionScreenState extends ConsumerState<RandomSelectionScreen> {
|
||||
double _distanceValue = 500;
|
||||
final List<String> _selectedCategories = [];
|
||||
|
||||
bool _isProcessingRecommendation = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: isDark ? AppColors.darkBackground : AppColors.lightBackground,
|
||||
backgroundColor: isDark
|
||||
? AppColors.darkBackground
|
||||
: AppColors.lightBackground,
|
||||
appBar: AppBar(
|
||||
title: const Text('오늘 뭐 먹Z?'),
|
||||
backgroundColor: isDark ? AppColors.darkPrimary : AppColors.lightPrimary,
|
||||
backgroundColor: isDark
|
||||
? AppColors.darkPrimary
|
||||
: AppColors.lightPrimary,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
),
|
||||
@@ -58,37 +70,36 @@ class _RandomSelectionScreenState extends ConsumerState<RandomSelectionScreen> {
|
||||
const SizedBox(height: 12),
|
||||
Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final restaurantsAsync = ref.watch(restaurantListProvider);
|
||||
final restaurantsAsync = ref.watch(
|
||||
restaurantListProvider,
|
||||
);
|
||||
return restaurantsAsync.when(
|
||||
data: (restaurants) => Text(
|
||||
'${restaurants.length}개',
|
||||
style: AppTypography.heading1(isDark).copyWith(
|
||||
color: AppColors.lightPrimary,
|
||||
),
|
||||
style: AppTypography.heading1(
|
||||
isDark,
|
||||
).copyWith(color: AppColors.lightPrimary),
|
||||
),
|
||||
loading: () => const CircularProgressIndicator(
|
||||
color: AppColors.lightPrimary,
|
||||
),
|
||||
error: (_, __) => Text(
|
||||
'0개',
|
||||
style: AppTypography.heading1(isDark).copyWith(
|
||||
color: AppColors.lightPrimary,
|
||||
),
|
||||
style: AppTypography.heading1(
|
||||
isDark,
|
||||
).copyWith(color: AppColors.lightPrimary),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Text(
|
||||
'등록된 맛집',
|
||||
style: AppTypography.body2(isDark),
|
||||
),
|
||||
Text('등록된 맛집', style: AppTypography.body2(isDark)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
|
||||
// 날씨 정보 카드
|
||||
Card(
|
||||
color: isDark ? AppColors.darkSurface : AppColors.lightSurface,
|
||||
@@ -109,7 +120,9 @@ class _RandomSelectionScreenState extends ConsumerState<RandomSelectionScreen> {
|
||||
Container(
|
||||
width: 1,
|
||||
height: 50,
|
||||
color: isDark ? AppColors.darkDivider : AppColors.lightDivider,
|
||||
color: isDark
|
||||
? AppColors.darkDivider
|
||||
: AppColors.lightDivider,
|
||||
),
|
||||
_buildWeatherData('1시간 후', weather.nextHour, isDark),
|
||||
],
|
||||
@@ -122,13 +135,27 @@ class _RandomSelectionScreenState extends ConsumerState<RandomSelectionScreen> {
|
||||
error: (_, __) => Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_buildWeatherInfo('지금', Icons.wb_sunny, '맑음', 20, isDark),
|
||||
_buildWeatherInfo(
|
||||
'지금',
|
||||
Icons.wb_sunny,
|
||||
'맑음',
|
||||
20,
|
||||
isDark,
|
||||
),
|
||||
Container(
|
||||
width: 1,
|
||||
height: 50,
|
||||
color: isDark ? AppColors.darkDivider : AppColors.lightDivider,
|
||||
color: isDark
|
||||
? AppColors.darkDivider
|
||||
: AppColors.lightDivider,
|
||||
),
|
||||
_buildWeatherInfo(
|
||||
'1시간 후',
|
||||
Icons.wb_sunny,
|
||||
'맑음',
|
||||
22,
|
||||
isDark,
|
||||
),
|
||||
_buildWeatherInfo('1시간 후', Icons.wb_sunny, '맑음', 22, isDark),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -136,9 +163,9 @@ class _RandomSelectionScreenState extends ConsumerState<RandomSelectionScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
|
||||
// 거리 설정 카드
|
||||
Card(
|
||||
color: isDark ? AppColors.darkSurface : AppColors.lightSurface,
|
||||
@@ -151,10 +178,7 @@ class _RandomSelectionScreenState extends ConsumerState<RandomSelectionScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'최대 거리',
|
||||
style: AppTypography.heading2(isDark),
|
||||
),
|
||||
Text('최대 거리', style: AppTypography.heading2(isDark)),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
@@ -162,7 +186,8 @@ class _RandomSelectionScreenState extends ConsumerState<RandomSelectionScreen> {
|
||||
child: SliderTheme(
|
||||
data: SliderTheme.of(context).copyWith(
|
||||
activeTrackColor: AppColors.lightPrimary,
|
||||
inactiveTrackColor: AppColors.lightPrimary.withValues(alpha: 0.3),
|
||||
inactiveTrackColor: AppColors.lightPrimary
|
||||
.withValues(alpha: 0.3),
|
||||
thumbColor: AppColors.lightPrimary,
|
||||
trackHeight: 4,
|
||||
),
|
||||
@@ -180,22 +205,27 @@ class _RandomSelectionScreenState extends ConsumerState<RandomSelectionScreen> {
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'${_distanceValue.toInt()}m',
|
||||
style: AppTypography.body1(isDark).copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
style: AppTypography.body1(
|
||||
isDark,
|
||||
).copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final locationAsync = ref.watch(currentLocationProvider);
|
||||
final restaurantsAsync = ref.watch(restaurantListProvider);
|
||||
|
||||
if (locationAsync.hasValue && restaurantsAsync.hasValue) {
|
||||
final locationAsync = ref.watch(
|
||||
currentLocationProvider,
|
||||
);
|
||||
final restaurantsAsync = ref.watch(
|
||||
restaurantListProvider,
|
||||
);
|
||||
|
||||
if (locationAsync.hasValue &&
|
||||
restaurantsAsync.hasValue) {
|
||||
final location = locationAsync.value;
|
||||
final restaurants = restaurantsAsync.value;
|
||||
|
||||
|
||||
if (location != null && restaurants != null) {
|
||||
final count = _getRestaurantCountInRange(
|
||||
restaurants,
|
||||
@@ -208,7 +238,7 @@ class _RandomSelectionScreenState extends ConsumerState<RandomSelectionScreen> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return Text(
|
||||
'위치 정보를 가져오는 중...',
|
||||
style: AppTypography.caption(isDark),
|
||||
@@ -219,9 +249,9 @@ class _RandomSelectionScreenState extends ConsumerState<RandomSelectionScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
|
||||
// 카테고리 선택 카드
|
||||
Card(
|
||||
color: isDark ? AppColors.darkSurface : AppColors.lightSurface,
|
||||
@@ -234,22 +264,26 @@ class _RandomSelectionScreenState extends ConsumerState<RandomSelectionScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'카테고리',
|
||||
style: AppTypography.heading2(isDark),
|
||||
),
|
||||
Text('카테고리', style: AppTypography.heading2(isDark)),
|
||||
const SizedBox(height: 12),
|
||||
Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final categoriesAsync = ref.watch(categoriesProvider);
|
||||
|
||||
|
||||
return categoriesAsync.when(
|
||||
data: (categories) => Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: categories.isEmpty
|
||||
? [const Text('카테고리 없음')]
|
||||
: categories.map((category) => _buildCategoryChip(category, isDark)).toList(),
|
||||
: categories
|
||||
.map(
|
||||
(category) => _buildCategoryChip(
|
||||
category,
|
||||
isDark,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
loading: () => const CircularProgressIndicator(),
|
||||
error: (_, __) => const Text('카테고리를 불러올 수 없습니다'),
|
||||
@@ -260,12 +294,14 @@ class _RandomSelectionScreenState extends ConsumerState<RandomSelectionScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
|
||||
// 추천받기 버튼
|
||||
ElevatedButton(
|
||||
onPressed: _canRecommend() ? _startRecommendation : null,
|
||||
onPressed: !_isProcessingRecommendation && _canRecommend()
|
||||
? () => _startRecommendation()
|
||||
: null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.lightPrimary,
|
||||
foregroundColor: Colors.white,
|
||||
@@ -275,27 +311,36 @@ class _RandomSelectionScreenState extends ConsumerState<RandomSelectionScreen> {
|
||||
),
|
||||
elevation: 3,
|
||||
),
|
||||
child: const Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.play_arrow, size: 28),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'광고보고 추천받기',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
child: _isProcessingRecommendation
|
||||
? const SizedBox(
|
||||
height: 24,
|
||||
width: 24,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2.5,
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: const Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.play_arrow, size: 28),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'광고보고 추천받기',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Widget _buildWeatherData(String label, WeatherData weatherData, bool isDark) {
|
||||
return Column(
|
||||
children: [
|
||||
@@ -309,47 +354,42 @@ class _RandomSelectionScreenState extends ConsumerState<RandomSelectionScreen> {
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${weatherData.temperature}°C',
|
||||
style: AppTypography.body1(isDark).copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
weatherData.description,
|
||||
style: AppTypography.caption(isDark),
|
||||
style: AppTypography.body1(
|
||||
isDark,
|
||||
).copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(weatherData.description, style: AppTypography.caption(isDark)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWeatherInfo(String label, IconData icon, String description, int temperature, bool isDark) {
|
||||
|
||||
Widget _buildWeatherInfo(
|
||||
String label,
|
||||
IconData icon,
|
||||
String description,
|
||||
int temperature,
|
||||
bool isDark,
|
||||
) {
|
||||
return Column(
|
||||
children: [
|
||||
Text(label, style: AppTypography.caption(isDark)),
|
||||
const SizedBox(height: 8),
|
||||
Icon(
|
||||
icon,
|
||||
color: Colors.orange,
|
||||
size: 32,
|
||||
),
|
||||
Icon(icon, color: Colors.orange, size: 32),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'$temperature°C',
|
||||
style: AppTypography.body1(isDark).copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
description,
|
||||
style: AppTypography.caption(isDark),
|
||||
style: AppTypography.body1(
|
||||
isDark,
|
||||
).copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(description, style: AppTypography.caption(isDark)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
Widget _buildCategoryChip(String category, bool isDark) {
|
||||
final isSelected = _selectedCategories.contains(category);
|
||||
|
||||
|
||||
return FilterChip(
|
||||
label: Text(category),
|
||||
selected: isSelected,
|
||||
@@ -362,18 +402,24 @@ class _RandomSelectionScreenState extends ConsumerState<RandomSelectionScreen> {
|
||||
}
|
||||
});
|
||||
},
|
||||
backgroundColor: isDark ? AppColors.darkSurface : AppColors.lightBackground,
|
||||
backgroundColor: isDark
|
||||
? AppColors.darkSurface
|
||||
: AppColors.lightBackground,
|
||||
selectedColor: AppColors.lightPrimary.withValues(alpha: 0.2),
|
||||
checkmarkColor: AppColors.lightPrimary,
|
||||
labelStyle: TextStyle(
|
||||
color: isSelected ? AppColors.lightPrimary : (isDark ? AppColors.darkTextPrimary : AppColors.lightTextPrimary),
|
||||
color: isSelected
|
||||
? AppColors.lightPrimary
|
||||
: (isDark ? AppColors.darkTextPrimary : AppColors.lightTextPrimary),
|
||||
),
|
||||
side: BorderSide(
|
||||
color: isSelected ? AppColors.lightPrimary : (isDark ? AppColors.darkDivider : AppColors.lightDivider),
|
||||
color: isSelected
|
||||
? AppColors.lightPrimary
|
||||
: (isDark ? AppColors.darkDivider : AppColors.lightDivider),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
int _getRestaurantCountInRange(
|
||||
List<Restaurant> restaurants,
|
||||
Position location,
|
||||
@@ -389,62 +435,163 @@ class _RandomSelectionScreenState extends ConsumerState<RandomSelectionScreen> {
|
||||
return distance <= maxDistance;
|
||||
}).length;
|
||||
}
|
||||
|
||||
|
||||
bool _canRecommend() {
|
||||
final locationAsync = ref.read(currentLocationProvider);
|
||||
final restaurantsAsync = ref.read(restaurantListProvider);
|
||||
|
||||
if (!locationAsync.hasValue || !restaurantsAsync.hasValue) return false;
|
||||
|
||||
|
||||
if (!locationAsync.hasValue || !restaurantsAsync.hasValue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final location = locationAsync.value;
|
||||
final restaurants = restaurantsAsync.value;
|
||||
|
||||
if (location == null || restaurants == null || restaurants.isEmpty) return false;
|
||||
|
||||
final count = _getRestaurantCountInRange(restaurants, location, _distanceValue);
|
||||
|
||||
if (location == null || restaurants == null || restaurants.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final count = _getRestaurantCountInRange(
|
||||
restaurants,
|
||||
location,
|
||||
_distanceValue,
|
||||
);
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
Future<void> _startRecommendation() async {
|
||||
|
||||
Future<void> _startRecommendation({bool skipAd = false}) async {
|
||||
if (_isProcessingRecommendation) return;
|
||||
|
||||
setState(() {
|
||||
_isProcessingRecommendation = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final candidate = await _generateRecommendationCandidate();
|
||||
if (candidate == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!skipAd) {
|
||||
final adService = ref.read(adServiceProvider);
|
||||
// Ad dialog 자체가 비동기 동작을 포함하므로 사용 후 mounted 체크를 수행한다.
|
||||
// ignore: use_build_context_synchronously
|
||||
final adWatched = await adService.showInterstitialAd(context);
|
||||
if (!mounted) return;
|
||||
if (!adWatched) {
|
||||
_showSnack(
|
||||
'광고를 끝까지 시청해야 추천을 받을 수 있어요.',
|
||||
backgroundColor: AppColors.lightError,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
_showRecommendationDialog(candidate);
|
||||
} catch (_) {
|
||||
_showSnack(
|
||||
'추천을 준비하는 중 문제가 발생했습니다.',
|
||||
backgroundColor: AppColors.lightError,
|
||||
);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isProcessingRecommendation = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<Restaurant?> _generateRecommendationCandidate() async {
|
||||
final notifier = ref.read(recommendationNotifierProvider.notifier);
|
||||
|
||||
|
||||
await notifier.getRandomRecommendation(
|
||||
maxDistance: _distanceValue,
|
||||
selectedCategories: _selectedCategories,
|
||||
);
|
||||
|
||||
|
||||
final result = ref.read(recommendationNotifierProvider);
|
||||
|
||||
result.whenData((restaurant) {
|
||||
if (restaurant != null && mounted) {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => RecommendationResultDialog(
|
||||
restaurant: restaurant,
|
||||
onReroll: () {
|
||||
Navigator.pop(context);
|
||||
_startRecommendation();
|
||||
},
|
||||
onConfirmVisit: () {
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('맛있게 드세요! 🍴'),
|
||||
backgroundColor: AppColors.lightPrimary,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
if (result.hasError) {
|
||||
final message = result.error?.toString() ?? '알 수 없는 오류';
|
||||
_showSnack(
|
||||
'추천 중 오류가 발생했습니다: $message',
|
||||
backgroundColor: AppColors.lightError,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
final restaurant = result.asData?.value;
|
||||
if (restaurant == null) {
|
||||
_showSnack('조건에 맞는 식당이 존재하지 않습니다', backgroundColor: AppColors.lightError);
|
||||
}
|
||||
return restaurant;
|
||||
}
|
||||
|
||||
void _showRecommendationDialog(Restaurant restaurant) {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (dialogContext) => RecommendationResultDialog(
|
||||
restaurant: restaurant,
|
||||
onReroll: () async {
|
||||
Navigator.pop(dialogContext);
|
||||
await _startRecommendation(skipAd: true);
|
||||
},
|
||||
onClose: () async {
|
||||
Navigator.pop(dialogContext);
|
||||
await _handleRecommendationAccepted(restaurant);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _handleRecommendationAccepted(Restaurant restaurant) async {
|
||||
final recommendationTime = DateTime.now();
|
||||
|
||||
try {
|
||||
final notificationEnabled = await ref.read(
|
||||
notificationEnabledProvider.future,
|
||||
);
|
||||
if (notificationEnabled) {
|
||||
final delayMinutes = await ref.read(
|
||||
notificationDelayMinutesProvider.future,
|
||||
);
|
||||
} else if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('조건에 맞는 맛집이 없습니다'),
|
||||
backgroundColor: AppColors.lightError,
|
||||
),
|
||||
final notificationService = ref.read(notificationServiceProvider);
|
||||
await notificationService.scheduleVisitReminder(
|
||||
restaurantId: restaurant.id,
|
||||
restaurantName: restaurant.name,
|
||||
recommendationTime: recommendationTime,
|
||||
delayMinutes: delayMinutes,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
await ref
|
||||
.read(visitNotifierProvider.notifier)
|
||||
.createVisitFromRecommendation(
|
||||
restaurantId: restaurant.id,
|
||||
recommendationTime: recommendationTime,
|
||||
);
|
||||
|
||||
_showSnack('맛있게 드세요! 🍴');
|
||||
} catch (_) {
|
||||
_showSnack(
|
||||
'방문 기록 또는 알림 예약에 실패했습니다.',
|
||||
backgroundColor: AppColors.lightError,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _showSnack(
|
||||
String message, {
|
||||
Color backgroundColor = AppColors.lightPrimary,
|
||||
}) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context)
|
||||
..hideCurrentSnackBar()
|
||||
..showSnackBar(
|
||||
SnackBar(content: Text(message), backgroundColor: backgroundColor),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,24 @@
|
||||
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/notification_service.dart';
|
||||
import 'package:lunchpick/domain/entities/restaurant.dart';
|
||||
import 'package:lunchpick/presentation/providers/settings_provider.dart';
|
||||
import 'package:lunchpick/presentation/providers/visit_provider.dart';
|
||||
|
||||
class RecommendationResultDialog extends ConsumerWidget {
|
||||
class RecommendationResultDialog extends StatelessWidget {
|
||||
final Restaurant restaurant;
|
||||
final VoidCallback onReroll;
|
||||
final VoidCallback onConfirmVisit;
|
||||
final Future<void> Function() onReroll;
|
||||
final Future<void> Function() onClose;
|
||||
|
||||
const RecommendationResultDialog({
|
||||
super.key,
|
||||
required this.restaurant,
|
||||
required this.onReroll,
|
||||
required this.onConfirmVisit,
|
||||
required this.onClose,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
|
||||
return Dialog(
|
||||
backgroundColor: Colors.transparent,
|
||||
child: Container(
|
||||
@@ -56,9 +52,9 @@ class RecommendationResultDialog extends ConsumerWidget {
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'오늘의 추천!',
|
||||
style: AppTypography.heading2(false).copyWith(
|
||||
color: Colors.white,
|
||||
),
|
||||
style: AppTypography.heading2(
|
||||
false,
|
||||
).copyWith(color: Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -68,13 +64,15 @@ class RecommendationResultDialog extends ConsumerWidget {
|
||||
right: 8,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.close, color: Colors.white),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
onPressed: () async {
|
||||
await onClose();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
// 맛집 정보
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
@@ -90,24 +88,27 @@ class RecommendationResultDialog extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
|
||||
// 카테고리
|
||||
Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.lightPrimary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
'${restaurant.category} > ${restaurant.subCategory}',
|
||||
style: AppTypography.body2(isDark).copyWith(
|
||||
color: AppColors.lightPrimary,
|
||||
),
|
||||
style: AppTypography.body2(
|
||||
isDark,
|
||||
).copyWith(color: AppColors.lightPrimary),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
if (restaurant.description != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
@@ -116,18 +117,20 @@ class RecommendationResultDialog extends ConsumerWidget {
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
|
||||
|
||||
const SizedBox(height: 16),
|
||||
const Divider(),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
|
||||
// 주소
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.location_on,
|
||||
size: 20,
|
||||
color: isDark ? AppColors.darkTextSecondary : AppColors.lightTextSecondary,
|
||||
color: isDark
|
||||
? AppColors.darkTextSecondary
|
||||
: AppColors.lightTextSecondary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
@@ -138,7 +141,7 @@ class RecommendationResultDialog extends ConsumerWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
|
||||
if (restaurant.phoneNumber != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
@@ -146,7 +149,9 @@ class RecommendationResultDialog extends ConsumerWidget {
|
||||
Icon(
|
||||
Icons.phone,
|
||||
size: 20,
|
||||
color: isDark ? AppColors.darkTextSecondary : AppColors.lightTextSecondary,
|
||||
color: isDark
|
||||
? AppColors.darkTextSecondary
|
||||
: AppColors.lightTextSecondary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
@@ -156,18 +161,22 @@ class RecommendationResultDialog extends ConsumerWidget {
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
|
||||
// 버튼들
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: onReroll,
|
||||
onPressed: () async {
|
||||
await onReroll();
|
||||
},
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
side: const BorderSide(color: AppColors.lightPrimary),
|
||||
side: const BorderSide(
|
||||
color: AppColors.lightPrimary,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
@@ -182,29 +191,7 @@ class RecommendationResultDialog extends ConsumerWidget {
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () async {
|
||||
final recommendationTime = DateTime.now();
|
||||
|
||||
// 알림 설정 확인
|
||||
final notificationEnabled = await ref.read(notificationEnabledProvider.future);
|
||||
|
||||
if (notificationEnabled) {
|
||||
// 알림 예약
|
||||
final notificationService = NotificationService();
|
||||
await notificationService.scheduleVisitReminder(
|
||||
restaurantId: restaurant.id,
|
||||
restaurantName: restaurant.name,
|
||||
recommendationTime: recommendationTime,
|
||||
);
|
||||
}
|
||||
|
||||
// 방문 기록 자동 생성 (미확인 상태로)
|
||||
await ref.read(visitNotifierProvider.notifier).createVisitFromRecommendation(
|
||||
restaurantId: restaurant.id,
|
||||
recommendationTime: recommendationTime,
|
||||
);
|
||||
|
||||
// 기존 콜백 실행
|
||||
onConfirmVisit();
|
||||
await onClose();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
@@ -214,7 +201,7 @@ class RecommendationResultDialog extends ConsumerWidget {
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: const Text('여기로 갈게요!'),
|
||||
child: const Text('닫기'),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -227,4 +214,4 @@ class RecommendationResultDialog extends ConsumerWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user