feat(ads): 네이티브 광고 적용 및 디버그 스위치 이동

This commit is contained in:
JiWoong Sul
2025-12-03 17:25:00 +09:00
parent 5cae033977
commit d733bf664b
19 changed files with 461 additions and 168 deletions

View File

@@ -1,4 +1,3 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:table_calendar/table_calendar.dart';
@@ -7,13 +6,11 @@ import '../../../core/constants/app_typography.dart';
import '../../../domain/entities/recommendation_record.dart';
import '../../../domain/entities/visit_record.dart';
import '../../providers/recommendation_provider.dart';
import '../../providers/debug_test_data_provider.dart';
import '../../providers/visit_provider.dart';
import '../../widgets/native_ad_placeholder.dart';
import 'widgets/visit_record_card.dart';
import 'widgets/recommendation_record_card.dart';
import 'widgets/visit_statistics.dart';
import 'widgets/debug_test_data_banner.dart';
class CalendarScreen extends ConsumerStatefulWidget {
const CalendarScreen({super.key});
@@ -36,11 +33,6 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen>
_selectedDay = DateTime.now();
_focusedDay = DateTime.now();
_tabController = TabController(length: 2, vsync: this);
if (kDebugMode) {
Future.microtask(
() => ref.read(debugTestDataNotifierProvider.notifier).initialize(),
);
}
}
@override
@@ -115,10 +107,6 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen>
constraints: BoxConstraints(minHeight: constraints.maxHeight),
child: Column(
children: [
if (kDebugMode)
const DebugTestDataBanner(
margin: EdgeInsets.fromLTRB(16, 16, 16, 8),
),
Card(
margin: const EdgeInsets.all(16),
color: isDark
@@ -238,6 +226,7 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen>
const SizedBox(height: 16),
const NativeAdPlaceholder(
margin: EdgeInsets.fromLTRB(16, 0, 16, 16),
height: 360,
),
_buildDayRecords(_selectedDay, isDark),
],

View File

@@ -1,11 +1,9 @@
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/visit_provider.dart';
import 'package:lunchpick/presentation/providers/restaurant_provider.dart';
import 'package:lunchpick/presentation/pages/calendar/widgets/debug_test_data_banner.dart';
import 'package:lunchpick/presentation/widgets/native_ad_placeholder.dart';
class VisitStatistics extends ConsumerWidget {
@@ -35,15 +33,11 @@ class VisitStatistics extends ConsumerWidget {
padding: const EdgeInsets.all(16),
child: Column(
children: [
if (kDebugMode) ...[
const DebugTestDataBanner(margin: EdgeInsets.zero),
const SizedBox(height: 12),
],
// 이번 달 통계
_buildMonthlyStats(monthlyStatsAsync, isDark),
const SizedBox(height: 16),
const NativeAdPlaceholder(),
const NativeAdPlaceholder(height: 360),
const SizedBox(height: 16),
// 주간 통계 차트

View File

@@ -13,7 +13,6 @@ import '../../providers/location_provider.dart';
import '../../providers/recommendation_provider.dart';
import '../../providers/restaurant_provider.dart';
import '../../providers/weather_provider.dart';
import '../../widgets/native_ad_placeholder.dart';
import 'widgets/recommendation_result_dialog.dart';
class RandomSelectionScreen extends ConsumerStatefulWidget {
@@ -133,51 +132,65 @@ class _RandomSelectionScreenState extends ConsumerState<RandomSelectionScreen> {
child: Consumer(
builder: (context, ref, child) {
final weatherAsync = ref.watch(weatherProvider);
const double sectionHeight = 112;
return weatherAsync.when(
data: (weather) => Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildWeatherData('지금', weather.current, isDark),
Container(
width: 1,
height: 50,
color: isDark
? AppColors.darkDivider
: AppColors.lightDivider,
),
_buildWeatherData('1시간 후', weather.nextHour, isDark),
],
),
loading: () => const Center(
child: CircularProgressIndicator(
color: AppColors.lightPrimary,
data: (weather) => SizedBox(
height: sectionHeight,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildWeatherData('지금', weather.current, isDark),
Container(
width: 1,
height: 50,
color: isDark
? AppColors.darkDivider
: AppColors.lightDivider,
),
_buildWeatherData(
'1시간 후',
weather.nextHour,
isDark,
),
],
),
),
error: (_, __) => Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildWeatherInfo(
'지금',
Icons.wb_sunny,
'맑음',
20,
isDark,
loading: () => const SizedBox(
height: sectionHeight,
child: Center(
child: CircularProgressIndicator(
color: AppColors.lightPrimary,
),
Container(
width: 1,
height: 50,
color: isDark
? AppColors.darkDivider
: AppColors.lightDivider,
),
_buildWeatherInfo(
'1시간 후',
Icons.wb_sunny,
'맑음',
22,
isDark,
),
],
),
),
error: (_, __) => SizedBox(
height: sectionHeight,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildWeatherInfo(
'지금',
Icons.wb_sunny,
'맑음',
20,
isDark,
),
Container(
width: 1,
height: 50,
color: isDark
? AppColors.darkDivider
: AppColors.lightDivider,
),
_buildWeatherInfo(
'1시간 후',
Icons.wb_sunny,
'맑음',
22,
isDark,
),
],
),
),
);
},
@@ -385,9 +398,9 @@ class _RandomSelectionScreenState extends ConsumerState<RandomSelectionScreen> {
),
const SizedBox(height: 16),
const NativeAdPlaceholder(
margin: EdgeInsets.symmetric(vertical: 8),
),
// const NativeAdPlaceholder(
// margin: EdgeInsets.symmetric(vertical: 8),
// ),
],
),
),

View File

@@ -145,18 +145,33 @@ class _RestaurantListScreenState extends ConsumerState<RestaurantListScreen> {
return _buildEmptyState(isDark);
}
const adInterval = 6; // 5리스트 후 1광고
const adOffset = 5; // 1~5 리스트 이후 6 광고 시작
final adCount = (items.length ~/ adOffset);
final totalCount = items.length + adCount;
return ListView.builder(
itemCount: items.length + 1,
itemCount: totalCount,
itemBuilder: (context, index) {
if (index == 0) {
final isAdIndex =
index >= adOffset &&
(index - adOffset) % adInterval == 0;
if (isAdIndex) {
return const NativeAdPlaceholder(
margin: EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
height: 200, // 카드 높이와 비슷한 중간 사이즈
);
}
final item = items[index - 1];
final adsBefore = index < adOffset
? 0
: ((index - adOffset) ~/ adInterval) + 1;
final itemIndex = index - adsBefore;
final item = items[itemIndex];
return RestaurantCard(
restaurant: item.restaurant,
distanceKm: item.distanceKm,

View File

@@ -5,6 +5,8 @@ import 'package:adaptive_theme/adaptive_theme.dart';
import 'package:permission_handler/permission_handler.dart';
import '../../../core/constants/app_colors.dart';
import '../../../core/constants/app_typography.dart';
import '../../providers/debug_share_preview_provider.dart';
import '../../providers/debug_test_data_provider.dart';
import '../../providers/settings_provider.dart';
import '../../providers/notification_provider.dart';
@@ -24,6 +26,11 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
void initState() {
super.initState();
_loadSettings();
if (kDebugMode) {
Future.microtask(
() => ref.read(debugTestDataNotifierProvider.notifier).initialize(),
);
}
}
Future<void> _loadSettings() async {
@@ -343,6 +350,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
],
),
),
if (kDebugMode) _buildDebugToolsCard(isDark),
], isDark),
const SizedBox(height: 24),
@@ -444,6 +452,81 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
}
}
Widget _buildDebugToolsCard(bool isDark) {
final sharePreviewEnabled = ref.watch(debugSharePreviewProvider);
final testDataState = ref.watch(debugTestDataNotifierProvider);
final testDataNotifier = ref.read(debugTestDataNotifierProvider.notifier);
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
color: isDark ? AppColors.darkSurface : AppColors.lightSurface,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Column(
children: [
ListTile(
leading: const Icon(
Icons.wifi_tethering,
color: AppColors.lightPrimary,
),
title: const Text('공유 테스트 모드'),
subtitle: const Text('광고·권한 없이 디버그 샘플 코드/기기를 표시'),
trailing: Switch.adaptive(
value: sharePreviewEnabled,
onChanged: (value) {
ref.read(debugSharePreviewProvider.notifier).state = value;
},
activeColor: AppColors.lightPrimary,
),
),
const Divider(height: 1),
ListTile(
leading: const Icon(
Icons.science_outlined,
color: AppColors.lightPrimary,
),
title: const Text('기록/통계 테스트 데이터'),
subtitle: Text(
testDataState.isEnabled
? '테스트 데이터가 적용되었습니다 (디버그 전용)'
: '디버그 빌드에서만 사용 가능합니다.',
),
trailing: testDataState.isProcessing
? const SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Switch.adaptive(
value: testDataState.isEnabled,
onChanged: (value) async {
if (value) {
await testDataNotifier.enableTestData();
} else {
await testDataNotifier.disableTestData();
}
},
activeColor: AppColors.lightPrimary,
),
),
if (testDataState.errorMessage != null)
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
testDataState.errorMessage!,
style: AppTypography.caption(isDark).copyWith(
color: AppColors.lightError,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
);
}
void _showPermissionDialog(String permissionName) {
final isDark = Theme.of(context).brightness == Brightness.dark;

View File

@@ -12,6 +12,7 @@ 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/debug_share_preview_provider.dart';
import 'package:lunchpick/presentation/providers/restaurant_provider.dart';
import 'package:lunchpick/presentation/widgets/native_ad_placeholder.dart';
import 'package:uuid/uuid.dart';
@@ -85,9 +86,9 @@ class _ShareScreenState extends ConsumerState<ShareScreen> {
bool _isScanning = false;
List<ShareDevice>? _nearbyDevices;
StreamSubscription<String>? _dataSubscription;
ProviderSubscription<bool>? _debugPreviewSub;
final _uuid = const Uuid();
bool _debugPreviewEnabled = false;
bool _debugPreviewProcessing = false;
Timer? _debugPreviewTimer;
@override
@@ -97,11 +98,25 @@ class _ShareScreenState extends ConsumerState<ShareScreen> {
_dataSubscription = bluetoothService.onDataReceived.listen((payload) {
_handleIncomingData(payload);
});
_debugPreviewEnabled = ref.read(debugSharePreviewProvider);
if (_debugPreviewEnabled) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_handleDebugToggleChange(true);
});
}
_debugPreviewSub = ref.listenManual<bool>(debugSharePreviewProvider, (
previous,
next,
) {
if (previous == next) return;
_handleDebugToggleChange(next);
});
}
@override
void dispose() {
_dataSubscription?.cancel();
_debugPreviewSub?.close();
ref.read(bluetoothServiceProvider).stopListening();
_debugPreviewTimer?.cancel();
super.dispose();
@@ -132,10 +147,6 @@ class _ShareScreenState extends ConsumerState<ShareScreen> {
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (kDebugMode) ...[
_buildDebugToggle(isDark),
const SizedBox(height: 16),
],
_ShareCard(
isDark: isDark,
icon: Icons.upload_rounded,
@@ -146,7 +157,7 @@ class _ShareScreenState extends ConsumerState<ShareScreen> {
child: _buildSendSection(isDark),
),
const SizedBox(height: 16),
const NativeAdPlaceholder(),
const NativeAdPlaceholder(height: 220),
const SizedBox(height: 16),
_ShareCard(
isDark: isDark,
@@ -577,87 +588,16 @@ class _ShareScreenState extends ConsumerState<ShareScreen> {
);
}
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;
Future<void> _handleDebugToggleChange(bool enabled) async {
if (!mounted) return;
setState(() {
_debugPreviewProcessing = true;
_debugPreviewEnabled = enabled;
});
if (enabled) {
await _startDebugPreviewFlow();
} else {
_stopDebugPreviewFlow();
}
if (!mounted) return;
setState(() {
_debugPreviewEnabled = enabled;
_debugPreviewProcessing = false;
});
}
Future<void> _startDebugPreviewFlow() async {