feat(app): finalize ad gated flows and weather
- add AppLogger and replace scattered print logging\n- implement ad-gated recommendation flow with reminder handling and calendar link\n- complete Bluetooth share pipeline with ad gate and merge\n- integrate KMA weather API with caching and dart-define decoding\n- add NaverUrlProcessor refactor and restore restaurant repository tests
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
@@ -28,6 +29,7 @@ class RandomSelectionScreen extends ConsumerStatefulWidget {
|
||||
class _RandomSelectionScreenState extends ConsumerState<RandomSelectionScreen> {
|
||||
double _distanceValue = 500;
|
||||
final List<String> _selectedCategories = [];
|
||||
final List<String> _excludedRestaurantIds = [];
|
||||
bool _isProcessingRecommendation = false;
|
||||
|
||||
@override
|
||||
@@ -459,18 +461,28 @@ class _RandomSelectionScreenState extends ConsumerState<RandomSelectionScreen> {
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
Future<void> _startRecommendation({bool skipAd = false}) async {
|
||||
Future<void> _startRecommendation({
|
||||
bool skipAd = false,
|
||||
bool isReroll = false,
|
||||
}) async {
|
||||
if (_isProcessingRecommendation) return;
|
||||
|
||||
if (!isReroll) {
|
||||
_excludedRestaurantIds.clear();
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isProcessingRecommendation = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final candidate = await _generateRecommendationCandidate();
|
||||
final candidate = await _generateRecommendationCandidate(
|
||||
excludedRestaurantIds: _excludedRestaurantIds,
|
||||
);
|
||||
if (candidate == null) {
|
||||
return;
|
||||
}
|
||||
final recommendedAt = DateTime.now();
|
||||
|
||||
if (!skipAd) {
|
||||
final adService = ref.read(adServiceProvider);
|
||||
@@ -488,7 +500,7 @@ class _RandomSelectionScreenState extends ConsumerState<RandomSelectionScreen> {
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
_showRecommendationDialog(candidate);
|
||||
await _showRecommendationDialog(candidate, recommendedAt: recommendedAt);
|
||||
} catch (_) {
|
||||
_showSnack(
|
||||
'추천을 준비하는 중 문제가 발생했습니다.',
|
||||
@@ -503,12 +515,16 @@ class _RandomSelectionScreenState extends ConsumerState<RandomSelectionScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<Restaurant?> _generateRecommendationCandidate() async {
|
||||
Future<Restaurant?> _generateRecommendationCandidate({
|
||||
List<String> excludedRestaurantIds = const [],
|
||||
}) async {
|
||||
final notifier = ref.read(recommendationNotifierProvider.notifier);
|
||||
|
||||
await notifier.getRandomRecommendation(
|
||||
final recommendation = await notifier.getRandomRecommendation(
|
||||
maxDistance: _distanceValue,
|
||||
selectedCategories: _selectedCategories,
|
||||
excludedRestaurantIds: excludedRestaurantIds,
|
||||
shouldSaveRecord: false,
|
||||
);
|
||||
|
||||
final result = ref.read(recommendationNotifierProvider);
|
||||
@@ -522,49 +538,82 @@ class _RandomSelectionScreenState extends ConsumerState<RandomSelectionScreen> {
|
||||
return null;
|
||||
}
|
||||
|
||||
final restaurant = result.asData?.value;
|
||||
if (restaurant == null) {
|
||||
_showSnack('조건에 맞는 식당이 존재하지 않습니다', backgroundColor: AppColors.lightError);
|
||||
if (recommendation == null) {
|
||||
_showSnack(
|
||||
'조건에 맞는 식당이 존재하지 않습니다. 광고는 재생되지 않았습니다.',
|
||||
backgroundColor: AppColors.lightError,
|
||||
);
|
||||
}
|
||||
return restaurant;
|
||||
return recommendation;
|
||||
}
|
||||
|
||||
void _showRecommendationDialog(Restaurant restaurant) {
|
||||
showDialog(
|
||||
Future<void> _showRecommendationDialog(
|
||||
Restaurant restaurant, {
|
||||
DateTime? recommendedAt,
|
||||
}) async {
|
||||
final result = await showDialog<RecommendationDialogResult>(
|
||||
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);
|
||||
},
|
||||
),
|
||||
builder: (dialogContext) =>
|
||||
RecommendationResultDialog(restaurant: restaurant),
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
switch (result) {
|
||||
case RecommendationDialogResult.reroll:
|
||||
setState(() {
|
||||
_excludedRestaurantIds.add(restaurant.id);
|
||||
});
|
||||
await _startRecommendation(skipAd: true, isReroll: true);
|
||||
break;
|
||||
case RecommendationDialogResult.confirm:
|
||||
case RecommendationDialogResult.autoConfirm:
|
||||
default:
|
||||
await _handleRecommendationAccepted(
|
||||
restaurant,
|
||||
recommendedAt ?? DateTime.now(),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleRecommendationAccepted(Restaurant restaurant) async {
|
||||
final recommendationTime = DateTime.now();
|
||||
|
||||
Future<void> _handleRecommendationAccepted(
|
||||
Restaurant restaurant,
|
||||
DateTime recommendationTime,
|
||||
) async {
|
||||
try {
|
||||
final recommendationNotifier = ref.read(
|
||||
recommendationNotifierProvider.notifier,
|
||||
);
|
||||
await recommendationNotifier.saveRecommendationRecord(
|
||||
restaurant,
|
||||
recommendationTime: recommendationTime,
|
||||
);
|
||||
|
||||
final notificationEnabled = await ref.read(
|
||||
notificationEnabledProvider.future,
|
||||
);
|
||||
if (notificationEnabled) {
|
||||
final delayMinutes = await ref.read(
|
||||
notificationDelayMinutesProvider.future,
|
||||
);
|
||||
bool notificationScheduled = false;
|
||||
if (notificationEnabled && !kIsWeb) {
|
||||
final notificationService = ref.read(notificationServiceProvider);
|
||||
await notificationService.scheduleVisitReminder(
|
||||
restaurantId: restaurant.id,
|
||||
restaurantName: restaurant.name,
|
||||
recommendationTime: recommendationTime,
|
||||
delayMinutes: delayMinutes,
|
||||
final notificationReady = await notificationService.ensureInitialized(
|
||||
requestPermission: true,
|
||||
);
|
||||
|
||||
if (notificationReady) {
|
||||
final delayMinutes = await ref.read(
|
||||
notificationDelayMinutesProvider.future,
|
||||
);
|
||||
await notificationService.scheduleVisitReminder(
|
||||
restaurantId: restaurant.id,
|
||||
restaurantName: restaurant.name,
|
||||
recommendationTime: recommendationTime,
|
||||
delayMinutes: delayMinutes,
|
||||
);
|
||||
notificationScheduled = await notificationService
|
||||
.hasVisitReminderScheduled();
|
||||
}
|
||||
}
|
||||
|
||||
await ref
|
||||
@@ -574,7 +623,19 @@ class _RandomSelectionScreenState extends ConsumerState<RandomSelectionScreen> {
|
||||
recommendationTime: recommendationTime,
|
||||
);
|
||||
|
||||
_showSnack('맛있게 드세요! 🍴');
|
||||
if (notificationEnabled && !notificationScheduled && !kIsWeb) {
|
||||
_showSnack(
|
||||
'방문 기록은 저장됐지만 알림 권한이나 설정을 확인해 주세요. 방문 알림을 예약하지 못했습니다.',
|
||||
backgroundColor: AppColors.lightError,
|
||||
);
|
||||
} else {
|
||||
_showSnack('맛있게 드세요! 🍴');
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_excludedRestaurantIds.clear();
|
||||
});
|
||||
}
|
||||
} catch (_) {
|
||||
_showSnack(
|
||||
'방문 기록 또는 알림 예약에 실패했습니다.',
|
||||
@@ -588,10 +649,22 @@ class _RandomSelectionScreenState extends ConsumerState<RandomSelectionScreen> {
|
||||
Color backgroundColor = AppColors.lightPrimary,
|
||||
}) {
|
||||
if (!mounted) return;
|
||||
final topInset = MediaQuery.of(context).viewPadding.top;
|
||||
ScaffoldMessenger.of(context)
|
||||
..hideCurrentSnackBar()
|
||||
..showSnackBar(
|
||||
SnackBar(content: Text(message), backgroundColor: backgroundColor),
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
backgroundColor: backgroundColor,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
margin: EdgeInsets.fromLTRB(
|
||||
16,
|
||||
(topInset > 0 ? topInset : 16) + 8,
|
||||
16,
|
||||
0,
|
||||
),
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user