feat(ads/seo/release): v1.2.0 with live Google Ads, isolated ad profile, ads.txt, and SEO/AEO/GEO optimization

This commit is contained in:
Yun Chan 2026-09-14 00:39:44 +09:00
parent a88aafa1e5
commit 0e568f1f0a
975 changed files with 130593 additions and 16783 deletions

View file

@ -0,0 +1,294 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
using Paca.App.Core;
namespace Paca.App.Services;
/// <summary>
/// PACA 데스크톱 애플리케이션 화면 디버깅 캡처 서비스.
/// Win32 GDI / PrintWindow / RenderTargetBitmap 하이브리드 파이프라인으로
/// WPF 및 WebView2(HWND) 자식 컨트롤이 포함된 실제 앱 윈도우 전체를 스크린샷으로 캡처하여 저장한다.
/// </summary>
public static class AppDebugScreenshotService
{
[StructLayout(LayoutKind.Sequential)]
private struct RECT
{
public int Left;
public int Top;
public int Right;
public int Bottom;
}
[DllImport("user32.dll")]
private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
[DllImport("user32.dll")]
private static extern bool PrintWindow(IntPtr hWnd, IntPtr hdcBkgnd, uint nFlags);
[DllImport("user32.dll")]
private static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll")]
private static extern bool BringWindowToTop(IntPtr hWnd);
[DllImport("user32.dll")]
private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
private const int SW_RESTORE = 9;
private const uint PW_RENDERFULLCONTENT = 0x00000002;
private static DispatcherTimer? _triggerTimer;
public static string GetDefaultScreenshotPath() =>
Path.Combine(AppPaths.DataDir, "debug_screenshot.png");
public static string ResolveTriggerPath(string? input)
{
if (string.IsNullOrWhiteSpace(input))
return GetDefaultScreenshotPath();
return input.Trim();
}
/// <summary>
/// 대상 Window의 현재 화면을 PNG 파일로 캡처하여 저장한다.
/// </summary>
public static bool CaptureWindow(Window window, string outputPath)
{
if (window == null) return false;
try
{
var dir = Path.GetDirectoryName(outputPath);
if (!string.IsNullOrEmpty(dir))
{
Directory.CreateDirectory(dir);
}
var helper = new WindowInteropHelper(window);
var hWnd = helper.Handle;
if (hWnd != IntPtr.Zero && GetWindowRect(hWnd, out var rect))
{
try
{
ShowWindow(hWnd, SW_RESTORE);
SetForegroundWindow(hWnd);
BringWindowToTop(hWnd);
System.Threading.Thread.Sleep(200);
GetWindowRect(hWnd, out rect);
}
catch { }
int width = rect.Right - rect.Left;
int height = rect.Bottom - rect.Top;
if (width > 0 && height > 0)
{
try
{
using var bmp = new System.Drawing.Bitmap(width, height);
using (var g = System.Drawing.Graphics.FromImage(bmp))
{
var hdc = g.GetHdc();
try
{
PrintWindow(hWnd, hdc, PW_RENDERFULLCONTENT);
}
finally
{
g.ReleaseHdc(hdc);
}
// WebView2 자식 컨트롤 실시간 렌더링 화면 비트맵 오버레이
OverlayAdBanners(window, g, width, height);
}
bmp.Save(outputPath, System.Drawing.Imaging.ImageFormat.Png);
if (File.Exists(outputPath) && new FileInfo(outputPath).Length > 0)
{
return true;
}
}
catch
{
// GDI 캡처 실패 시 하단 WPF RenderTargetBitmap 폴백 진행
}
}
}
// 폴백: 순수 WPF 비주얼 트리 렌더링
int rtbWidth = (int)Math.Max(1, window.ActualWidth > 0 ? window.ActualWidth : window.Width);
int rtbHeight = (int)Math.Max(1, window.ActualHeight > 0 ? window.ActualHeight : window.Height);
if (rtbWidth <= 1) rtbWidth = 1024;
if (rtbHeight <= 1) rtbHeight = 768;
var rtb = new RenderTargetBitmap(rtbWidth, rtbHeight, 96, 96, PixelFormats.Pbgra32);
rtb.Render(window);
var encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(rtb));
using (var fs = File.Create(outputPath))
{
encoder.Save(fs);
}
return File.Exists(outputPath) && new FileInfo(outputPath).Length > 0;
}
catch
{
return false;
}
}
/// <summary>
/// 외부 신호 파일(%AppData%/VideoDownloader/screenshot_trigger.req)을 감지하여
/// 백그라운드에서 실시간 스크린샷을 찍도록 타이머를 구동한다.
/// </summary>
public static void StartFileTriggerWatcher(Window window, Action<string>? onCaptured = null)
{
if (_triggerTimer != null) return;
var triggerFile = Path.Combine(AppPaths.DataDir, "screenshot_trigger.req");
_triggerTimer = new DispatcherTimer
{
Interval = TimeSpan.FromSeconds(1)
};
_triggerTimer.Tick += (_, _) =>
{
try
{
if (File.Exists(triggerFile))
{
string target = "";
try
{
target = File.ReadAllText(triggerFile);
File.Delete(triggerFile);
}
catch
{
// 읽기 경합 방어
return;
}
var savePath = ResolveTriggerPath(target);
bool ok = CaptureWindow(window, savePath);
if (ok)
{
onCaptured?.Invoke(savePath);
}
}
}
catch
{
// 백그라운드 폴러 방어
}
};
_triggerTimer.Start();
}
/// <summary>
/// 앱 실행 후 指定된 지연 시간(기본 3초) 뒤에 자동으로 스크린샷을 찍어 저장한다.
/// </summary>
public static void ScheduleAutoCapture(Window window, int delayMs = 3000, string? targetPath = null)
{
Task.Delay(delayMs).ContinueWith(_ =>
{
window.Dispatcher.Invoke(() =>
{
var dest = ResolveTriggerPath(targetPath);
CaptureWindow(window, dest);
});
});
}
private static void OverlayAdBanners(Window window, System.Drawing.Graphics g, int windowWidth, int windowHeight)
{
try
{
window.Dispatcher.Invoke(() =>
{
var banners = FindVisualChildren<Paca.App.Controls.DesktopAdBannerControl>(window);
foreach (var banner in banners)
{
if (banner.Visibility != Visibility.Visible || banner.ActualWidth <= 0 || banner.ActualHeight <= 0)
continue;
var previewPath = Path.Combine(AppPaths.DataDir, "ad_previews", $"ad_{banner.Placement}.png");
if (!File.Exists(previewPath)) continue;
try
{
var transform = banner.TransformToAncestor(window);
var point = transform.Transform(new Point(0, 0));
using var fs = new FileStream(previewPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using var adImg = System.Drawing.Image.FromStream(fs);
int x = (int)point.X;
int y = (int)point.Y;
int w = (int)banner.ActualWidth;
int h = (int)banner.ActualHeight;
if (banner.Placement == Paca.Browser.Ads.DesktopAdPlacement.BottomAppBanner)
{
x += (w - 728) / 2;
y += (h - 90) / 2;
w = 728;
h = 90;
}
else if (banner.Placement == Paca.Browser.Ads.DesktopAdPlacement.DownloadViewBanner)
{
x += (w - 300) / 2;
y += (h - 250) / 2;
w = 300;
h = 250;
}
g.DrawImage(adImg, x, y, w, h);
}
catch (Exception ex)
{
try
{
var p = Path.Combine(AppPaths.DataDir, "ad_debug.log");
File.AppendAllText(p, $"[{DateTime.Now:HH:mm:ss.fff}] Overlay exception for {banner.Placement}: {ex.Message}\n");
}
catch { }
}
}
});
}
catch { }
}
private static IEnumerable<T> FindVisualChildren<T>(DependencyObject depObj) where T : DependencyObject
{
if (depObj == null) yield break;
int count = VisualTreeHelper.GetChildrenCount(depObj);
for (int i = 0; i < count; i++)
{
var child = VisualTreeHelper.GetChild(depObj, i);
if (child is T match)
{
yield return match;
}
foreach (var childOfChild in FindVisualChildren<T>(child))
{
yield return childOfChild;
}
}
}
}