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;
}
}