using System; using System.Diagnostics; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using Microsoft.Web.WebView2.Core; using Paca.Browser.Ads; using Wv2 = Microsoft.Web.WebView2.Wpf; namespace Paca.App.Controls; /// /// 데스크톱 브라우저 및 다운로드 뷰용 네이티브/실시간 웹 배너 컨트롤 (Designpaca 스위스 미니멀리즘) /// public partial class DesktopAdBannerControl : UserControl { private DesktopAdData? _adData; private DesktopAdPlacement _placement; private CoreWebView2Environment? _env; private Wv2.WebView2? _webView; private bool _isInitializing; private readonly System.Threading.SemaphoreSlim _captureLock = new(1, 1); public DesktopAdPlacement Placement { get => _placement; set { _placement = value; ApplyPlacementSizing(); } } public DesktopAdBannerControl() : this(DesktopAdPlacement.DownloadViewBanner) { } private static readonly object s_initLock = new(); public DesktopAdBannerControl(DesktopAdPlacement placement) { _placement = placement; lock (s_initLock) { InitializeComponent(); } Loaded += OnLoaded; } private void ApplyPlacementSizing() { if (PART_WebViewContainer == null || PART_RootBorder == null) return; PART_RootBorder.Padding = new Thickness(0); PART_RootBorder.Background = System.Windows.Media.Brushes.Transparent; PART_RootBorder.BorderThickness = new Thickness(0); if (_placement == DesktopAdPlacement.BottomAppBanner) { PART_WebViewContainer.Width = 728; PART_WebViewContainer.Height = 90; PART_WebViewContainer.MinHeight = 90; PART_RootBorder.Margin = new Thickness(0, 4, 0, 4); Width = 728; Height = 98; } else { PART_WebViewContainer.Width = 300; PART_WebViewContainer.Height = 250; PART_WebViewContainer.MinHeight = 250; PART_RootBorder.Margin = new Thickness(0, 4, 0, 4); Width = 300; Height = 258; } } private void LogAd(string msg) { try { var p = System.IO.Path.Combine(Paca.App.AppPaths.DataDir, "ad_debug.log"); System.IO.File.AppendAllText(p, $"[{DateTime.Now:HH:mm:ss.fff}] [{_placement}] {msg}\n"); } catch { } } /// /// 메인 윈도우의 공유 WebView2 환경을 주입하여 실제 구글 배너 광고 렌더링을 시작한다. /// public void AttachEnvironment(CoreWebView2Environment? env) { LogAd($"AttachEnvironment called. Env != null: {env != null}"); _env = env; if (_adData == null) { _adData = DesktopAdManager.Instance.GetAdForPlacement(_placement); } TryLoadRealAdAsync(); } private void OnLoaded(object sender, RoutedEventArgs e) { LogAd("OnLoaded called"); RefreshAd(); } public void RefreshAd() { _adData = DesktopAdManager.Instance.GetAdForPlacement(_placement); if (_adData == null || !_adData.IsVisible) { Visibility = Visibility.Collapsed; return; } Visibility = Visibility.Visible; ApplyPlacementSizing(); PART_BadgeText.Text = _adData.Badge; PART_TitleText.Text = _adData.Title; PART_AdvertiserText.Text = _adData.Advertiser; PART_DescText.Text = _adData.Description; PART_CtaText.Text = _adData.CtaText; PART_DismissBtn.Visibility = _adData.IsDismissible ? Visibility.Visible : Visibility.Collapsed; if (!string.IsNullOrWhiteSpace(_adData.IconOrImageUrl)) { try { var bitmap = new System.Windows.Media.Imaging.BitmapImage(); bitmap.BeginInit(); bitmap.UriSource = new Uri(_adData.IconOrImageUrl, UriKind.Absolute); bitmap.CacheOption = System.Windows.Media.Imaging.BitmapCacheOption.OnLoad; bitmap.EndInit(); PART_AdImage.Source = bitmap; PART_AdImage.Visibility = Visibility.Visible; PART_AdIconFallback.Visibility = Visibility.Collapsed; } catch { PART_AdImage.Visibility = Visibility.Collapsed; PART_AdIconFallback.Visibility = Visibility.Visible; } } else { PART_AdImage.Visibility = Visibility.Collapsed; PART_AdIconFallback.Visibility = Visibility.Visible; } // 노출(Impression) 트래킹 DesktopAdManager.Instance.TrackImpression(_adData.Id); if (_adData.IsRealAd) { TryLoadRealAdAsync(); } } private static Microsoft.Web.WebView2.Core.CoreWebView2Environment? _adEnvironment; private static async System.Threading.Tasks.Task GetOrCreateAdEnvironmentAsync() { if (_adEnvironment != null) return _adEnvironment; try { var adDataFolder = System.IO.Path.Combine(Paca.App.AppPaths.DataDir, "ad_profile"); System.IO.Directory.CreateDirectory(adDataFolder); var options = new Microsoft.Web.WebView2.Core.CoreWebView2EnvironmentOptions { AreBrowserExtensionsEnabled = false, AdditionalBrowserArguments = "--disable-extensions --allow-file-access-from-files" }; _adEnvironment = await Microsoft.Web.WebView2.Core.CoreWebView2Environment.CreateAsync( browserExecutableFolder: null, userDataFolder: adDataFolder, options: options); return _adEnvironment; } catch { return null; } } private async void TryLoadRealAdAsync() { if (_isInitializing) return; if (_adData == null) { _adData = DesktopAdManager.Instance.GetAdForPlacement(_placement); } if (_adData == null || !_adData.IsVisible) return; _isInitializing = true; try { if (_webView == null) { _webView = new Wv2.WebView2 { DefaultBackgroundColor = System.Drawing.Color.FromArgb(30, 32, 40) }; PART_WebViewContainer.Child = _webView; } if (_webView.CoreWebView2 == null) { var isolatedEnv = await GetOrCreateAdEnvironmentAsync().ConfigureAwait(true); var env = isolatedEnv ?? _env ?? MainWindow.SharedEnvironment; if (env != null) { LogAd($"EnsureCoreWebView2Async starting (isolated={isolatedEnv != null})..."); await _webView.EnsureCoreWebView2Async(env); LogAd("EnsureCoreWebView2Async completed!"); } else { LogAd("EnsureCoreWebView2Async aborted: env is null"); return; } } _webView.NavigationCompleted -= OnNavigationCompleted; _webView.NavigationCompleted += OnNavigationCompleted; _webView.WebMessageReceived -= OnWebMessageReceived; _webView.WebMessageReceived += OnWebMessageReceived; if (_webView.CoreWebView2 != null) { _webView.CoreWebView2.WebMessageReceived -= OnWebMessageReceived; _webView.CoreWebView2.WebMessageReceived += OnWebMessageReceived; } var isBottom = _placement == DesktopAdPlacement.BottomAppBanner; var adWidth = isBottom ? 728 : 300; var adHeight = isBottom ? 90 : 250; var divId = isBottom ? "div-gpt-ad-bottom" : "div-gpt-ad-banner"; if (_webView != null) { _webView.Width = adWidth; _webView.Height = adHeight; _webView.HorizontalAlignment = HorizontalAlignment.Center; _webView.VerticalAlignment = VerticalAlignment.Center; } var slotId = !string.IsNullOrWhiteSpace(_adData.AdUnitId) ? _adData.AdUnitId : (isBottom ? DesktopAdManager.Instance.Config.EffectiveNtpBannerUnitId : DesktopAdManager.Instance.Config.EffectiveDownloadBannerUnitId); var snippet = _adData.HtmlEmbedSnippet ?? DesktopAdConfig.GenerateGoogleTagSnippet(slotId, adWidth, adHeight, divId); var pageHtml = GenerateRealAdHtml(_adData, _placement, snippet, adWidth, adHeight); var adDir = System.IO.Path.Combine(Paca.App.AppPaths.DataDir, "ad_html"); System.IO.Directory.CreateDirectory(adDir); var tempAdHtmlPath = System.IO.Path.Combine(adDir, $"ad_{_placement}.html"); System.IO.File.WriteAllText(tempAdHtmlPath, pageHtml, new System.Text.UTF8Encoding(true)); // Google 크롤러 및 퍼블리셔 인증용 ads.txt 및 app-ads.txt를 가상 호스트 디렉터리에도 동기화 const string adsTxtLine = "google.com, pub-1039714767792854, DIRECT, f08c47fec0942fa0\n"; try { var localAdsTxt = System.IO.Path.Combine(adDir, "ads.txt"); var localAppAdsTxt = System.IO.Path.Combine(adDir, "app-ads.txt"); if (!System.IO.File.Exists(localAdsTxt)) System.IO.File.WriteAllText(localAdsTxt, adsTxtLine, new System.Text.UTF8Encoding(false)); if (!System.IO.File.Exists(localAppAdsTxt)) System.IO.File.WriteAllText(localAppAdsTxt, adsTxtLine, new System.Text.UTF8Encoding(false)); } catch { } ApplyPlacementSizing(); PART_RealAdHost.Visibility = Visibility.Visible; PART_NativeCard.Visibility = Visibility.Collapsed; var cacheBuster = DateTime.UtcNow.Ticks; var virtualUrl = $"https://ads.paca.cloud/ad_{_placement}.html?v={cacheBuster}"; var fileUrl = new Uri(tempAdHtmlPath).AbsoluteUri + $"?v={cacheBuster}"; string targetUrl = virtualUrl; try { _webView?.CoreWebView2?.SetVirtualHostNameToFolderMapping( "ads.paca.cloud", adDir, Microsoft.Web.WebView2.Core.CoreWebView2HostResourceAccessKind.Allow); } catch (Exception ex) { LogAd($"SetVirtualHostNameToFolderMapping warning: {ex.Message}, fallback to fileUrl"); targetUrl = fileUrl; } LogAd($"Loading real Google ad: {targetUrl} (file fallback: {fileUrl})"); try { _webView?.CoreWebView2?.Navigate(targetUrl); } catch (Exception ex) { LogAd($"Navigate target failed ({ex.Message}), trying fileUrl fallback..."); try { _webView?.CoreWebView2?.Navigate(fileUrl); } catch { _webView?.CoreWebView2?.NavigateToString(pageHtml); } } } catch (Exception ex) { LogAd($"TryLoadRealAdAsync EXCEPTION: {ex}"); PART_RealAdHost.Visibility = Visibility.Collapsed; PART_NativeCard.Visibility = Visibility.Visible; } finally { _isInitializing = false; } } private static string GenerateRealAdHtml(DesktopAdData ad, DesktopAdPlacement placement, string snippet, int width, int height) { return $@" PACA Google Ad
{snippet}
"; } private async void OnNavigationCompleted(object? sender, Microsoft.Web.WebView2.Core.CoreWebView2NavigationCompletedEventArgs e) { LogAd($"OnNavigationCompleted: IsSuccess={e.IsSuccess}, WebErrorStatus={e.WebErrorStatus}, HttpStatusCode={e.HttpStatusCode}"); if (e.IsSuccess) { try { var previewsDir = System.IO.Path.Combine(Paca.App.AppPaths.DataDir, "ad_previews"); var previewFile = System.IO.Path.Combine(previewsDir, $"ad_{_placement}.png"); // 1차: 기본 DOM 준비 대기 await System.Threading.Tasks.Task.Delay(1000); await CaptureAdPreviewAsync(previewFile); // 2차: Google 원격 실시간 광고(gpt.js/doubleclick) 네트워크 다운로드 및 iframe 렌더링 완료 대기 후 정밀 캡처 await System.Threading.Tasks.Task.Delay(3000); await CaptureAdPreviewAsync(previewFile); } catch { } } } /// /// WebView2의 실제 렌더링 화면 비트맵을 직접 캡처하여 PNG 파일로 저장한다. /// public async System.Threading.Tasks.Task CaptureAdPreviewAsync(string outputPath) { if (_webView?.CoreWebView2 == null) return false; await _captureLock.WaitAsync(); try { var dir = System.IO.Path.GetDirectoryName(outputPath); if (!string.IsNullOrEmpty(dir)) System.IO.Directory.CreateDirectory(dir); using var stream = System.IO.File.Create(outputPath); await _webView.CoreWebView2.CapturePreviewAsync(Microsoft.Web.WebView2.Core.CoreWebView2CapturePreviewImageFormat.Png, stream); LogAd($"CaptureAdPreviewAsync saved: {outputPath}"); return System.IO.File.Exists(outputPath) && new System.IO.FileInfo(outputPath).Length > 0; } catch (Exception ex) { LogAd($"CaptureAdPreviewAsync EXCEPTION: {ex}"); return false; } finally { _captureLock.Release(); } } private void OnWebMessageReceived(object? sender, Microsoft.Web.WebView2.Core.CoreWebView2WebMessageReceivedEventArgs e) { try { var msg = e.TryGetWebMessageAsString(); if (string.IsNullOrWhiteSpace(msg)) return; LogAd($"OnWebMessageReceived: {msg}"); using var doc = System.Text.Json.JsonDocument.Parse(msg); if (doc.RootElement.TryGetProperty("type", out var typeProp)) { var type = typeProp.GetString(); if (type == "JS_CONSOLE") { var level = doc.RootElement.TryGetProperty("level", out var l) ? l.GetString() : "LOG"; var text = doc.RootElement.TryGetProperty("msg", out var m) ? m.GetString() : ""; LogAd($"[Browser {level}] {text}"); return; } if (type == "AD_RENDERED") { LogAd($"AD_RENDERED event received from Google ad slot!"); _ = System.Threading.Tasks.Task.Run(async () => { await System.Threading.Tasks.Task.Delay(500); var previewsDir = System.IO.Path.Combine(Paca.App.AppPaths.DataDir, "ad_previews"); var previewFile = System.IO.Path.Combine(previewsDir, $"ad_{_placement}.png"); await Dispatcher.InvokeAsync(() => CaptureAdPreviewAsync(previewFile)); }); return; } if (type == "AD_CLICK") { Dispatcher.Invoke(() => HandleAction()); return; } if (type == "AD_DISMISS") { Dispatcher.Invoke(() => { if (_adData != null) DesktopAdManager.Instance.DismissAd(_adData.Id); Visibility = Visibility.Collapsed; }); return; } } Dispatcher.Invoke(() => { PART_RealAdHost.Visibility = Visibility.Visible; PART_NativeCard.Visibility = Visibility.Collapsed; }); } catch (Exception ex) { LogAd($"OnWebMessageReceived EXCEPTION: {ex}"); } } public void OnCtaClicked(object sender, RoutedEventArgs e) { HandleAction(); } public void OnCardClicked(object sender, MouseButtonEventArgs e) { HandleAction(); } private void HandleAction() { if (_adData == null) return; DesktopAdManager.Instance.TrackClick(_adData.Id); if (!string.IsNullOrWhiteSpace(_adData.TargetUrl)) { try { Process.Start(new ProcessStartInfo { FileName = _adData.TargetUrl, UseShellExecute = true }); } catch { // 실패 방어 } } } public void OnDismissClicked(object sender, RoutedEventArgs e) { if (_adData == null) return; DesktopAdManager.Instance.DismissAd(_adData.Id); Visibility = Visibility.Collapsed; } }