feat(ads): 네이티브 광고 적용 및 디버그 스위치 이동
This commit is contained in:
@@ -29,6 +29,10 @@ android {
|
|||||||
targetSdk = flutter.targetSdkVersion
|
targetSdk = flutter.targetSdkVersion
|
||||||
versionCode = flutter.versionCode
|
versionCode = flutter.versionCode
|
||||||
versionName = flutter.versionName
|
versionName = flutter.versionName
|
||||||
|
|
||||||
|
manifestPlaceholders["admobAppId"] =
|
||||||
|
project.findProperty("ADMOB_APP_ID")
|
||||||
|
?: "ca-app-pub-3940256099942544~3347511713"
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
|||||||
@@ -21,6 +21,9 @@
|
|||||||
android:label="lunchpick"
|
android:label="lunchpick"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/ic_launcher">
|
||||||
|
<meta-data
|
||||||
|
android:name="com.google.android.gms.ads.APPLICATION_ID"
|
||||||
|
android:value="${admobAppId}" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||||
#include "Generated.xcconfig"
|
#include "Generated.xcconfig"
|
||||||
|
|
||||||
|
GAD_APPLICATION_ID=ca-app-pub-3940256099942544~1458002511
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||||
#include "Generated.xcconfig"
|
#include "Generated.xcconfig"
|
||||||
|
|
||||||
|
GAD_APPLICATION_ID=ca-app-pub-3940256099942544~1458002511
|
||||||
|
|||||||
@@ -54,5 +54,7 @@
|
|||||||
<string>맛집과의 거리 계산을 위해 위치 정보가 필요합니다.</string>
|
<string>맛집과의 거리 계산을 위해 위치 정보가 필요합니다.</string>
|
||||||
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||||
<string>맛집과의 거리 계산을 위해 위치 정보가 필요합니다.</string>
|
<string>맛집과의 거리 계산을 위해 위치 정보가 필요합니다.</string>
|
||||||
|
<key>GADApplicationIdentifier</key>
|
||||||
|
<string>$(GAD_APPLICATION_ID)</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -19,6 +19,14 @@ class AppConstants {
|
|||||||
static const String iosAdAppId = 'ca-app-pub-3940256099942544~1458002511';
|
static const String iosAdAppId = 'ca-app-pub-3940256099942544~1458002511';
|
||||||
static const String interstitialAdUnitId =
|
static const String interstitialAdUnitId =
|
||||||
'ca-app-pub-3940256099942544/1033173712';
|
'ca-app-pub-3940256099942544/1033173712';
|
||||||
|
static const String androidNativeAdUnitId =
|
||||||
|
'ca-app-pub-6691216385521068/7939870622';
|
||||||
|
static const String iosNativeAdUnitId =
|
||||||
|
'ca-app-pub-6691216385521068/7939870622';
|
||||||
|
static const String testAndroidNativeAdUnitId =
|
||||||
|
'ca-app-pub-3940256099942544/2247696110';
|
||||||
|
static const String testIosNativeAdUnitId =
|
||||||
|
'ca-app-pub-3940256099942544/3986624511';
|
||||||
|
|
||||||
// Hive Box Names
|
// Hive Box Names
|
||||||
static const String restaurantBox = 'restaurants';
|
static const String restaurantBox = 'restaurants';
|
||||||
|
|||||||
28
lib/core/utils/ad_helper.dart
Normal file
28
lib/core/utils/ad_helper.dart
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
import '../constants/app_constants.dart';
|
||||||
|
|
||||||
|
class AdHelper {
|
||||||
|
static bool get isMobilePlatform {
|
||||||
|
if (kIsWeb) return false;
|
||||||
|
return defaultTargetPlatform == TargetPlatform.android ||
|
||||||
|
defaultTargetPlatform == TargetPlatform.iOS;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String get nativeAdUnitId {
|
||||||
|
if (!isMobilePlatform) {
|
||||||
|
throw UnsupportedError('Native ads are only supported on mobile.');
|
||||||
|
}
|
||||||
|
|
||||||
|
final isIOS = defaultTargetPlatform == TargetPlatform.iOS;
|
||||||
|
if (isIOS) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
return AppConstants.testIosNativeAdUnitId;
|
||||||
|
}
|
||||||
|
return AppConstants.iosNativeAdUnitId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Android는 디버그/릴리즈 모두 실제 광고 단위 ID 사용
|
||||||
|
return AppConstants.androidNativeAdUnitId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,11 +4,13 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:adaptive_theme/adaptive_theme.dart';
|
import 'package:adaptive_theme/adaptive_theme.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:google_mobile_ads/google_mobile_ads.dart';
|
||||||
import 'package:timezone/data/latest_all.dart' as tz;
|
import 'package:timezone/data/latest_all.dart' as tz;
|
||||||
|
|
||||||
import 'core/constants/app_colors.dart';
|
import 'core/constants/app_colors.dart';
|
||||||
import 'core/constants/app_constants.dart';
|
import 'core/constants/app_constants.dart';
|
||||||
import 'core/services/notification_service.dart';
|
import 'core/services/notification_service.dart';
|
||||||
|
import 'core/utils/ad_helper.dart';
|
||||||
import 'domain/entities/restaurant.dart';
|
import 'domain/entities/restaurant.dart';
|
||||||
import 'domain/entities/visit_record.dart';
|
import 'domain/entities/visit_record.dart';
|
||||||
import 'domain/entities/recommendation_record.dart';
|
import 'domain/entities/recommendation_record.dart';
|
||||||
@@ -20,6 +22,10 @@ import 'data/sample/sample_data_initializer.dart';
|
|||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
if (AdHelper.isMobilePlatform) {
|
||||||
|
await MobileAds.instance.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize timezone
|
// Initialize timezone
|
||||||
tz.initializeTimeZones();
|
tz.initializeTimeZones();
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:table_calendar/table_calendar.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/recommendation_record.dart';
|
||||||
import '../../../domain/entities/visit_record.dart';
|
import '../../../domain/entities/visit_record.dart';
|
||||||
import '../../providers/recommendation_provider.dart';
|
import '../../providers/recommendation_provider.dart';
|
||||||
import '../../providers/debug_test_data_provider.dart';
|
|
||||||
import '../../providers/visit_provider.dart';
|
import '../../providers/visit_provider.dart';
|
||||||
import '../../widgets/native_ad_placeholder.dart';
|
import '../../widgets/native_ad_placeholder.dart';
|
||||||
import 'widgets/visit_record_card.dart';
|
import 'widgets/visit_record_card.dart';
|
||||||
import 'widgets/recommendation_record_card.dart';
|
import 'widgets/recommendation_record_card.dart';
|
||||||
import 'widgets/visit_statistics.dart';
|
import 'widgets/visit_statistics.dart';
|
||||||
import 'widgets/debug_test_data_banner.dart';
|
|
||||||
|
|
||||||
class CalendarScreen extends ConsumerStatefulWidget {
|
class CalendarScreen extends ConsumerStatefulWidget {
|
||||||
const CalendarScreen({super.key});
|
const CalendarScreen({super.key});
|
||||||
@@ -36,11 +33,6 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen>
|
|||||||
_selectedDay = DateTime.now();
|
_selectedDay = DateTime.now();
|
||||||
_focusedDay = DateTime.now();
|
_focusedDay = DateTime.now();
|
||||||
_tabController = TabController(length: 2, vsync: this);
|
_tabController = TabController(length: 2, vsync: this);
|
||||||
if (kDebugMode) {
|
|
||||||
Future.microtask(
|
|
||||||
() => ref.read(debugTestDataNotifierProvider.notifier).initialize(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -115,10 +107,6 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen>
|
|||||||
constraints: BoxConstraints(minHeight: constraints.maxHeight),
|
constraints: BoxConstraints(minHeight: constraints.maxHeight),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
if (kDebugMode)
|
|
||||||
const DebugTestDataBanner(
|
|
||||||
margin: EdgeInsets.fromLTRB(16, 16, 16, 8),
|
|
||||||
),
|
|
||||||
Card(
|
Card(
|
||||||
margin: const EdgeInsets.all(16),
|
margin: const EdgeInsets.all(16),
|
||||||
color: isDark
|
color: isDark
|
||||||
@@ -238,6 +226,7 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen>
|
|||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const NativeAdPlaceholder(
|
const NativeAdPlaceholder(
|
||||||
margin: EdgeInsets.fromLTRB(16, 0, 16, 16),
|
margin: EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||||
|
height: 360,
|
||||||
),
|
),
|
||||||
_buildDayRecords(_selectedDay, isDark),
|
_buildDayRecords(_selectedDay, isDark),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:lunchpick/core/constants/app_colors.dart';
|
import 'package:lunchpick/core/constants/app_colors.dart';
|
||||||
import 'package:lunchpick/core/constants/app_typography.dart';
|
import 'package:lunchpick/core/constants/app_typography.dart';
|
||||||
import 'package:lunchpick/presentation/providers/visit_provider.dart';
|
import 'package:lunchpick/presentation/providers/visit_provider.dart';
|
||||||
import 'package:lunchpick/presentation/providers/restaurant_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';
|
import 'package:lunchpick/presentation/widgets/native_ad_placeholder.dart';
|
||||||
|
|
||||||
class VisitStatistics extends ConsumerWidget {
|
class VisitStatistics extends ConsumerWidget {
|
||||||
@@ -35,15 +33,11 @@ class VisitStatistics extends ConsumerWidget {
|
|||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
if (kDebugMode) ...[
|
|
||||||
const DebugTestDataBanner(margin: EdgeInsets.zero),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
],
|
|
||||||
// 이번 달 통계
|
// 이번 달 통계
|
||||||
_buildMonthlyStats(monthlyStatsAsync, isDark),
|
_buildMonthlyStats(monthlyStatsAsync, isDark),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
const NativeAdPlaceholder(),
|
const NativeAdPlaceholder(height: 360),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// 주간 통계 차트
|
// 주간 통계 차트
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import '../../providers/location_provider.dart';
|
|||||||
import '../../providers/recommendation_provider.dart';
|
import '../../providers/recommendation_provider.dart';
|
||||||
import '../../providers/restaurant_provider.dart';
|
import '../../providers/restaurant_provider.dart';
|
||||||
import '../../providers/weather_provider.dart';
|
import '../../providers/weather_provider.dart';
|
||||||
import '../../widgets/native_ad_placeholder.dart';
|
|
||||||
import 'widgets/recommendation_result_dialog.dart';
|
import 'widgets/recommendation_result_dialog.dart';
|
||||||
|
|
||||||
class RandomSelectionScreen extends ConsumerStatefulWidget {
|
class RandomSelectionScreen extends ConsumerStatefulWidget {
|
||||||
@@ -133,8 +132,11 @@ class _RandomSelectionScreenState extends ConsumerState<RandomSelectionScreen> {
|
|||||||
child: Consumer(
|
child: Consumer(
|
||||||
builder: (context, ref, child) {
|
builder: (context, ref, child) {
|
||||||
final weatherAsync = ref.watch(weatherProvider);
|
final weatherAsync = ref.watch(weatherProvider);
|
||||||
|
const double sectionHeight = 112;
|
||||||
return weatherAsync.when(
|
return weatherAsync.when(
|
||||||
data: (weather) => Row(
|
data: (weather) => SizedBox(
|
||||||
|
height: sectionHeight,
|
||||||
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
_buildWeatherData('지금', weather.current, isDark),
|
_buildWeatherData('지금', weather.current, isDark),
|
||||||
@@ -145,15 +147,25 @@ class _RandomSelectionScreenState extends ConsumerState<RandomSelectionScreen> {
|
|||||||
? AppColors.darkDivider
|
? AppColors.darkDivider
|
||||||
: AppColors.lightDivider,
|
: AppColors.lightDivider,
|
||||||
),
|
),
|
||||||
_buildWeatherData('1시간 후', weather.nextHour, isDark),
|
_buildWeatherData(
|
||||||
|
'1시간 후',
|
||||||
|
weather.nextHour,
|
||||||
|
isDark,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
loading: () => const Center(
|
),
|
||||||
|
loading: () => const SizedBox(
|
||||||
|
height: sectionHeight,
|
||||||
|
child: Center(
|
||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
color: AppColors.lightPrimary,
|
color: AppColors.lightPrimary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
error: (_, __) => Row(
|
),
|
||||||
|
error: (_, __) => SizedBox(
|
||||||
|
height: sectionHeight,
|
||||||
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
_buildWeatherInfo(
|
_buildWeatherInfo(
|
||||||
@@ -179,6 +191,7 @@ class _RandomSelectionScreenState extends ConsumerState<RandomSelectionScreen> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -385,9 +398,9 @@ class _RandomSelectionScreenState extends ConsumerState<RandomSelectionScreen> {
|
|||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const NativeAdPlaceholder(
|
// const NativeAdPlaceholder(
|
||||||
margin: EdgeInsets.symmetric(vertical: 8),
|
// margin: EdgeInsets.symmetric(vertical: 8),
|
||||||
),
|
// ),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -145,18 +145,33 @@ class _RestaurantListScreenState extends ConsumerState<RestaurantListScreen> {
|
|||||||
return _buildEmptyState(isDark);
|
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(
|
return ListView.builder(
|
||||||
itemCount: items.length + 1,
|
itemCount: totalCount,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
if (index == 0) {
|
final isAdIndex =
|
||||||
|
index >= adOffset &&
|
||||||
|
(index - adOffset) % adInterval == 0;
|
||||||
|
if (isAdIndex) {
|
||||||
return const NativeAdPlaceholder(
|
return const NativeAdPlaceholder(
|
||||||
margin: EdgeInsets.symmetric(
|
margin: EdgeInsets.symmetric(
|
||||||
horizontal: 16,
|
horizontal: 16,
|
||||||
vertical: 8,
|
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(
|
return RestaurantCard(
|
||||||
restaurant: item.restaurant,
|
restaurant: item.restaurant,
|
||||||
distanceKm: item.distanceKm,
|
distanceKm: item.distanceKm,
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import 'package:adaptive_theme/adaptive_theme.dart';
|
|||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
import '../../../core/constants/app_colors.dart';
|
import '../../../core/constants/app_colors.dart';
|
||||||
import '../../../core/constants/app_typography.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/settings_provider.dart';
|
||||||
import '../../providers/notification_provider.dart';
|
import '../../providers/notification_provider.dart';
|
||||||
|
|
||||||
@@ -24,6 +26,11 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_loadSettings();
|
_loadSettings();
|
||||||
|
if (kDebugMode) {
|
||||||
|
Future.microtask(
|
||||||
|
() => ref.read(debugTestDataNotifierProvider.notifier).initialize(),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadSettings() async {
|
Future<void> _loadSettings() async {
|
||||||
@@ -343,6 +350,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (kDebugMode) _buildDebugToolsCard(isDark),
|
||||||
], isDark),
|
], isDark),
|
||||||
|
|
||||||
const SizedBox(height: 24),
|
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) {
|
void _showPermissionDialog(String permissionName) {
|
||||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import 'package:lunchpick/domain/entities/restaurant.dart';
|
|||||||
import 'package:lunchpick/domain/entities/share_device.dart';
|
import 'package:lunchpick/domain/entities/share_device.dart';
|
||||||
import 'package:lunchpick/presentation/providers/ad_provider.dart';
|
import 'package:lunchpick/presentation/providers/ad_provider.dart';
|
||||||
import 'package:lunchpick/presentation/providers/bluetooth_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/providers/restaurant_provider.dart';
|
||||||
import 'package:lunchpick/presentation/widgets/native_ad_placeholder.dart';
|
import 'package:lunchpick/presentation/widgets/native_ad_placeholder.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
@@ -85,9 +86,9 @@ class _ShareScreenState extends ConsumerState<ShareScreen> {
|
|||||||
bool _isScanning = false;
|
bool _isScanning = false;
|
||||||
List<ShareDevice>? _nearbyDevices;
|
List<ShareDevice>? _nearbyDevices;
|
||||||
StreamSubscription<String>? _dataSubscription;
|
StreamSubscription<String>? _dataSubscription;
|
||||||
|
ProviderSubscription<bool>? _debugPreviewSub;
|
||||||
final _uuid = const Uuid();
|
final _uuid = const Uuid();
|
||||||
bool _debugPreviewEnabled = false;
|
bool _debugPreviewEnabled = false;
|
||||||
bool _debugPreviewProcessing = false;
|
|
||||||
Timer? _debugPreviewTimer;
|
Timer? _debugPreviewTimer;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -97,11 +98,25 @@ class _ShareScreenState extends ConsumerState<ShareScreen> {
|
|||||||
_dataSubscription = bluetoothService.onDataReceived.listen((payload) {
|
_dataSubscription = bluetoothService.onDataReceived.listen((payload) {
|
||||||
_handleIncomingData(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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_dataSubscription?.cancel();
|
_dataSubscription?.cancel();
|
||||||
|
_debugPreviewSub?.close();
|
||||||
ref.read(bluetoothServiceProvider).stopListening();
|
ref.read(bluetoothServiceProvider).stopListening();
|
||||||
_debugPreviewTimer?.cancel();
|
_debugPreviewTimer?.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
@@ -132,10 +147,6 @@ class _ShareScreenState extends ConsumerState<ShareScreen> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
if (kDebugMode) ...[
|
|
||||||
_buildDebugToggle(isDark),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
],
|
|
||||||
_ShareCard(
|
_ShareCard(
|
||||||
isDark: isDark,
|
isDark: isDark,
|
||||||
icon: Icons.upload_rounded,
|
icon: Icons.upload_rounded,
|
||||||
@@ -146,7 +157,7 @@ class _ShareScreenState extends ConsumerState<ShareScreen> {
|
|||||||
child: _buildSendSection(isDark),
|
child: _buildSendSection(isDark),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const NativeAdPlaceholder(),
|
const NativeAdPlaceholder(height: 220),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_ShareCard(
|
_ShareCard(
|
||||||
isDark: isDark,
|
isDark: isDark,
|
||||||
@@ -577,87 +588,16 @@ class _ShareScreenState extends ConsumerState<ShareScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDebugToggle(bool isDark) {
|
Future<void> _handleDebugToggleChange(bool enabled) async {
|
||||||
return Card(
|
if (!mounted) return;
|
||||||
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(() {
|
setState(() {
|
||||||
_debugPreviewProcessing = true;
|
_debugPreviewEnabled = enabled;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
await _startDebugPreviewFlow();
|
await _startDebugPreviewFlow();
|
||||||
} else {
|
} else {
|
||||||
_stopDebugPreviewFlow();
|
_stopDebugPreviewFlow();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!mounted) return;
|
|
||||||
setState(() {
|
|
||||||
_debugPreviewEnabled = enabled;
|
|
||||||
_debugPreviewProcessing = false;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _startDebugPreviewFlow() async {
|
Future<void> _startDebugPreviewFlow() async {
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
final debugSharePreviewProvider = StateProvider<bool>((ref) => false);
|
||||||
@@ -1,24 +1,191 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_mobile_ads/google_mobile_ads.dart';
|
||||||
import 'package:lunchpick/core/constants/app_colors.dart';
|
import 'package:lunchpick/core/constants/app_colors.dart';
|
||||||
import 'package:lunchpick/core/constants/app_typography.dart';
|
import 'package:lunchpick/core/constants/app_typography.dart';
|
||||||
|
import 'package:lunchpick/core/utils/ad_helper.dart';
|
||||||
|
|
||||||
/// 네이티브 광고(Native Ad) 플레이스홀더
|
/// 실제 네이티브 광고(Native Ad)를 표시하는 영역.
|
||||||
class NativeAdPlaceholder extends StatelessWidget {
|
/// 광고 미지원 플랫폼이나 로드 실패 시 이전 플레이스홀더 스타일을 유지한다.
|
||||||
|
class NativeAdPlaceholder extends StatefulWidget {
|
||||||
final EdgeInsetsGeometry? margin;
|
final EdgeInsetsGeometry? margin;
|
||||||
final double height;
|
final double height;
|
||||||
|
final Duration refreshInterval;
|
||||||
|
|
||||||
const NativeAdPlaceholder({super.key, this.margin, this.height = 120});
|
const NativeAdPlaceholder({
|
||||||
|
super.key,
|
||||||
|
this.margin,
|
||||||
|
this.height = 200,
|
||||||
|
this.refreshInterval = const Duration(minutes: 2),
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<NativeAdPlaceholder> createState() => _NativeAdPlaceholderState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NativeAdPlaceholderState extends State<NativeAdPlaceholder> {
|
||||||
|
NativeAd? _nativeAd;
|
||||||
|
Timer? _refreshTimer;
|
||||||
|
bool _isLoading = false;
|
||||||
|
bool _isLoaded = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (!mounted) return;
|
||||||
|
_loadAd();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant NativeAdPlaceholder oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (widget.refreshInterval != oldWidget.refreshInterval && _isLoaded) {
|
||||||
|
_scheduleRefresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_refreshTimer?.cancel();
|
||||||
|
_nativeAd?.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _loadAd() {
|
||||||
|
if (!AdHelper.isMobilePlatform) return;
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
_refreshTimer?.cancel();
|
||||||
|
_nativeAd?.dispose();
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
_isLoaded = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
final adUnitId = AdHelper.nativeAdUnitId;
|
||||||
|
_nativeAd = NativeAd(
|
||||||
|
adUnitId: adUnitId,
|
||||||
|
request: const AdRequest(),
|
||||||
|
nativeTemplateStyle: _buildTemplateStyle(),
|
||||||
|
listener: NativeAdListener(
|
||||||
|
onAdLoaded: (ad) {
|
||||||
|
if (!mounted) {
|
||||||
|
ad.dispose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_isLoaded = true;
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
_scheduleRefresh();
|
||||||
|
},
|
||||||
|
onAdFailedToLoad: (ad, error) {
|
||||||
|
ad.dispose();
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
_isLoaded = false;
|
||||||
|
});
|
||||||
|
_scheduleRefresh(retry: true);
|
||||||
|
},
|
||||||
|
onAdClicked: (ad) => _scheduleRefresh(),
|
||||||
|
onAdOpened: (ad) => _scheduleRefresh(),
|
||||||
|
),
|
||||||
|
)..load();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _scheduleRefresh({bool retry = false}) {
|
||||||
|
_refreshTimer?.cancel();
|
||||||
|
if (!mounted) return;
|
||||||
|
final delay = retry ? const Duration(seconds: 30) : widget.refreshInterval;
|
||||||
|
_refreshTimer = Timer(delay, _loadAd);
|
||||||
|
}
|
||||||
|
|
||||||
|
NativeTemplateStyle _buildTemplateStyle() {
|
||||||
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
return NativeTemplateStyle(
|
||||||
|
templateType: TemplateType.medium,
|
||||||
|
mainBackgroundColor: isDark ? AppColors.darkSurface : Colors.white,
|
||||||
|
cornerRadius: 12,
|
||||||
|
callToActionTextStyle: NativeTemplateTextStyle(
|
||||||
|
textColor: Colors.white,
|
||||||
|
backgroundColor: AppColors.lightPrimary,
|
||||||
|
style: NativeTemplateFontStyle.bold,
|
||||||
|
),
|
||||||
|
primaryTextStyle: NativeTemplateTextStyle(
|
||||||
|
textColor: isDark ? Colors.white : Colors.black87,
|
||||||
|
style: NativeTemplateFontStyle.bold,
|
||||||
|
),
|
||||||
|
secondaryTextStyle: NativeTemplateTextStyle(
|
||||||
|
textColor: isDark ? Colors.white70 : Colors.black54,
|
||||||
|
style: NativeTemplateFontStyle.normal,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
|
||||||
|
if (!AdHelper.isMobilePlatform) {
|
||||||
|
return _buildPlaceholder(isDark, isLoading: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 250),
|
||||||
|
child: _isLoaded && _nativeAd != null
|
||||||
|
? _buildAdView(isDark)
|
||||||
|
: _buildPlaceholder(isDark, isLoading: _isLoading),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAdView(bool isDark) {
|
||||||
|
final containerHeight = max(widget.height, 200.0);
|
||||||
return Container(
|
return Container(
|
||||||
margin: margin ?? EdgeInsets.zero,
|
key: const ValueKey('nativeAdLoaded'),
|
||||||
padding: const EdgeInsets.all(16),
|
margin: widget.margin ?? EdgeInsets.zero,
|
||||||
height: height,
|
height: containerHeight,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
decoration: BoxDecoration(
|
decoration: _decoration(isDark),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
child: AdWidget(ad: _nativeAd!),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPlaceholder(bool isDark, {required bool isLoading}) {
|
||||||
|
final containerHeight = max(widget.height, 200.0);
|
||||||
|
return Container(
|
||||||
|
key: const ValueKey('nativeAdPlaceholder'),
|
||||||
|
margin: widget.margin ?? EdgeInsets.zero,
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
height: containerHeight,
|
||||||
|
width: double.infinity,
|
||||||
|
decoration: _decoration(isDark),
|
||||||
|
child: Center(
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.ad_units, color: AppColors.lightPrimary, size: 24),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
isLoading ? '광고 불러오는 중...' : '광고 영역',
|
||||||
|
style: AppTypography.heading2(isDark),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
BoxDecoration _decoration(bool isDark) {
|
||||||
|
return BoxDecoration(
|
||||||
color: isDark ? AppColors.darkSurface : Colors.white,
|
color: isDark ? AppColors.darkSurface : Colors.white,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
@@ -32,17 +199,6 @@ class NativeAdPlaceholder extends StatelessWidget {
|
|||||||
offset: const Offset(0, 4),
|
offset: const Offset(0, 4),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
child: Center(
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.ad_units, color: AppColors.lightPrimary, size: 24),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text('광고 영역', style: AppTypography.heading2(isDark)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import path_provider_foundation
|
|||||||
import share_plus
|
import share_plus
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
import url_launcher_macos
|
import url_launcher_macos
|
||||||
|
import webview_flutter_wkwebview
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
FlutterBluePlusPlugin.register(with: registry.registrar(forPlugin: "FlutterBluePlusPlugin"))
|
FlutterBluePlusPlugin.register(with: registry.registrar(forPlugin: "FlutterBluePlusPlugin"))
|
||||||
@@ -21,4 +22,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
|
WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin"))
|
||||||
}
|
}
|
||||||
|
|||||||
40
pubspec.lock
40
pubspec.lock
@@ -480,6 +480,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "13.2.5"
|
version: "13.2.5"
|
||||||
|
google_mobile_ads:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: google_mobile_ads
|
||||||
|
sha256: "0d4a3744b5e8ed1b8be6a1b452d309f811688855a497c6113fc4400f922db603"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.3.1"
|
||||||
graphs:
|
graphs:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1205,6 +1213,38 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.3"
|
version: "3.0.3"
|
||||||
|
webview_flutter:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: webview_flutter
|
||||||
|
sha256: c3e4fe614b1c814950ad07186007eff2f2e5dd2935eba7b9a9a1af8e5885f1ba
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.13.0"
|
||||||
|
webview_flutter_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: webview_flutter_android
|
||||||
|
sha256: "6ff028d8b8829b4e2482f7821dfb0b039f91c34a07075ed06f36172aa7afabda"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.10.10"
|
||||||
|
webview_flutter_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: webview_flutter_platform_interface
|
||||||
|
sha256: "63d26ee3aca7256a83ccb576a50272edd7cfc80573a4305caa98985feb493ee0"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.14.0"
|
||||||
|
webview_flutter_wkwebview:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: webview_flutter_wkwebview
|
||||||
|
sha256: a57b76a081bed3bf3a71a486bdf83642b00f1a7342043d50367cea68f338b1af
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.23.4"
|
||||||
win32:
|
win32:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -76,6 +76,9 @@ dependencies:
|
|||||||
timezone: ^0.9.2 # 시간대 처리
|
timezone: ^0.9.2 # 시간대 처리
|
||||||
workmanager: ^0.8.0 # 백그라운드 작업 (Android)
|
workmanager: ^0.8.0 # 백그라운드 작업 (Android)
|
||||||
|
|
||||||
|
# 광고
|
||||||
|
google_mobile_ads: ^5.1.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|||||||
Reference in New Issue
Block a user