using System.IO;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using SheetMe.Designer.Services;
using SheetMe.Designer.Views;
namespace SheetMe.Designer;
///
/// 서식생성기 애플리케이션 진입점.
/// 명령행: --render-smoke <입력.xml> <출력.png> — 첫 페이지를 오프스크린 렌더해 PNG 저장(검증용) 후 종료.
///
public partial class App : Application
{
///
/// 기동 — 진단 모드 분기, 아니면 HIS 세션을 확정한 뒤 메인 셸 표시.
/// HIS 기동 규약: SheetMe.Designer.exe "UidCod,ComNum,ShtCod" (콤마 구분 단일 인자).
///
/// Dispatcher 예외 폭주 차단용 — 10초 내 5회면 강제 종료(무한 팝업 루프 방지)
private readonly Queue recentCrashes = new();
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
// 진단 분기보다 먼저 등록한다 — 스모크 중 발생한 예외도 로그에 남아야 한다
Services.AppLog.Initialize();
DispatcherUnhandledException += OnDispatcherUnhandledException;
AppDomain.CurrentDomain.UnhandledException += (_, args) =>
Services.AppLog.Error("처리되지 않은 예외(도메인)", args.ExceptionObject as Exception ?? new Exception(args.ExceptionObject?.ToString()));
System.Threading.Tasks.TaskScheduler.UnobservedTaskException += (_, args) =>
{
Services.AppLog.Error("관측되지 않은 Task 예외", args.Exception);
args.SetObserved();
};
if (Services.StartupArguments.IsDiagnostic(e.Args))
{
Services.UserSession.InitializeDiagnostic();
Shutdown(RunDiagnostic(e.Args));
return;
}
var launch = Services.StartupArguments.Parse(e.Args);
var config = Services.ConfigService.Current;
if (!Services.SessionBootstrap.TryInitialize(launch, config.ConnectionString, config.DevUidCod, out var error))
{
MessageBox.Show(error, "서식생성기", MessageBoxButton.OK, MessageBoxImage.Error);
Shutdown(2);
return;
}
Services.ThemeManager.LoadSaved();
var main = new MainView();
if (launch?.ShtCod.Length > 0 && main.DataContext is ViewModels.MainViewModel viewModel)
{
// XAML DataContext 는 InitializeComponent 안에서 이미 생성되므로 Show 전에 넣을 수 있다.
// MainView/MainViewModel 생성자를 건드리지 않는 최소 경로.
viewModel.PendingSheetCode = launch.ShtCod;
}
main.Show();
}
///
/// UI 스레드 미처리 예외 — 기록 후 계속 진행한다. 편집 중 문서를 예외 하나로 잃지 않게 하려는 것이며,
/// 같은 예외가 반복되면(10초 내 5회) 무한 팝업 루프이므로 강제 종료한다.
///
private void OnDispatcherUnhandledException(object sender,
System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e)
{
var code = Services.AppLog.Error("처리되지 않은 예외(UI)", e.Exception);
var now = DateTime.UtcNow;
recentCrashes.Enqueue(now);
while (recentCrashes.Count > 0 && (now - recentCrashes.Peek()).TotalSeconds > 10)
{
recentCrashes.Dequeue();
}
if (recentCrashes.Count >= 5)
{
MessageBox.Show("반복되는 오류로 프로그램을 종료합니다.\n로그를 확인해 주세요.\n오류 코드: " + code,
"서식생성기", MessageBoxButton.OK, MessageBoxImage.Error);
e.Handled = true;
Shutdown(3);
return;
}
MessageBox.Show(
$"오류가 발생했지만 작업은 계속할 수 있습니다.\n저장하지 않은 내용이 있으면 먼저 저장해 주세요.\n\n오류 코드: {code}",
"서식생성기", MessageBoxButton.OK, MessageBoxImage.Warning);
e.Handled = true;
}
/// 진단 플래그 분기 — 종료 코드를 반환한다(호출부가 Shutdown 처리)
private int RunDiagnostic(string[] args)
{
if (args.Length >= 3 && args[0] == "--render-smoke")
{
return RunRenderSmoke(args[1], args[2]);
}
if (args.Length >= 2 && args[0] == "--edit-smoke")
{
return Diagnostics.EditSmoke.Run(args[1]);
}
if (args.Length >= 2 && args[0] == "--db-smoke")
{
var max = args.Length >= 3 && int.TryParse(args[2], out var n) ? n : 30;
return Diagnostics.DbSmoke.RunReadSmoke(args[1], max);
}
if (args.Length >= 3 && args[0] == "--db-save-smoke")
{
return Diagnostics.DbSmoke.RunSaveSmoke(args[1], args[2]);
}
if (args.Length >= 3 && args[0] == "--db-find")
{
return Diagnostics.DbSmoke.RunFind(args[1], args[2]);
}
if (args.Length >= 3 && args[0] == "--db-columns")
{
return Diagnostics.DbSmoke.RunColumns(args[1], args[2]);
}
if (args.Length >= 3 && args[0] == "--db-row")
{
return Diagnostics.DbSmoke.RunRow(args[1], args[2]);
}
if (args.Length >= 3 && args[0] == "--db-sample")
{
return Diagnostics.DbSmoke.RunSample(args[1], args[2]);
}
if (args.Length >= 3 && args[0] == "--db-word-smoke")
{
return Diagnostics.DbSmoke.RunWordSmoke(args[1], args[2]);
}
if (args.Length >= 3 && args[0] == "--db-spd")
{
return Diagnostics.DbSmoke.RunSpreadDump(args[1], args[2]);
}
if (args.Length >= 3 && args[0] == "--db-xml")
{
return Diagnostics.DbSmoke.RunDesignXml(args[1], args[2]);
}
if (args.Length >= 3 && args[0] == "--db-render")
{
return RunDbRenderSmoke(args[1], args[2]);
}
if (args.Length >= 2 && args[0] == "--db-gate")
{
return Diagnostics.DbSmoke.RunGateReport(args[1]);
}
if (args.Length >= 2 && args[0] == "--db-cls")
{
return Diagnostics.DbSmoke.RunClassReport(args[1]);
}
if (args.Length >= 3 && args[0] == "--db-tables")
{
return Diagnostics.DbSmoke.RunTableSearch(args[1], args[2]);
}
MessageBox.Show($"알 수 없는 진단 옵션입니다: {args[0]}", "서식생성기",
MessageBoxButton.OK, MessageBoxImage.Warning);
return 2;
}
/// 첫 페이지 오프스크린 렌더 → PNG (렌더 파이프라인 자동 검증)
private static int RunRenderSmoke(string xmlPath, string pngPath)
{
try
{
var store = new Data.Stores.XmlFileFormStore();
var document = store.LoadFile(xmlPath);
var page = DocumentMapper.CreatePage(document.Pages[0], 0);
var view = new PageView { DataContext = page };
var size = new Size(page.WidthDip, page.HeightDip);
view.Measure(size);
view.Arrange(new Rect(size));
view.UpdateLayout();
var bitmap = new RenderTargetBitmap(
(int)Math.Ceiling(size.Width), (int)Math.Ceiling(size.Height), 96, 96, PixelFormats.Pbgra32);
bitmap.Render(view);
var encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bitmap));
using var stream = File.Create(pngPath);
encoder.Save(stream);
Console.WriteLine($"렌더 스모크 완료: {pngPath} ({size.Width}x{size.Height}, " +
$"페이지 {document.Pages.Count}, 경고 {document.Meta.ReadWarnings.Count})");
return 0;
}
catch (Exception ex)
{
Console.Error.WriteLine($"렌더 스모크 실패: {ex}");
return 1;
}
}
///
/// DB 서식 오프스크린 렌더 → PNG (Spread 격자 주입 경로 포함 검증).
/// 페이지가 여럿이면 이름_p1.png, 이름_p2.png … 로 전부 뽑는다 —
/// 운영 서식 128건이 다중 페이지라 첫 장만 봐서는 검증이 되지 않는다.
///
private static int RunDbRenderSmoke(string shtCod, string pngPath)
{
try
{
var dataBusiness = new DataBusiness.FormDesignDataBusiness();
var document = dataBusiness.OpenFromDb(shtCod);
if (document is null)
{
File.WriteAllText(pngPath + ".err.txt", $"활성 디자인 없음: {shtCod}");
return 1;
}
var designer = new ViewModels.DesignerViewModel(document, dataBusiness.LoadSpreadGrids(shtCod));
var directory = Path.GetDirectoryName(pngPath) ?? ".";
var stem = Path.GetFileNameWithoutExtension(pngPath);
for (var i = 0; i < designer.Pages.Count; i++)
{
var page = designer.Pages[i];
var view = new PageView { DataContext = page };
var size = new Size(page.WidthDip, page.HeightDip);
view.Measure(size);
view.Arrange(new Rect(size));
view.UpdateLayout();
var bitmap = new RenderTargetBitmap(
(int)Math.Ceiling(size.Width), (int)Math.Ceiling(size.Height), 96, 96, PixelFormats.Pbgra32);
bitmap.Render(view);
var encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bitmap));
var target = designer.Pages.Count == 1
? pngPath
: Path.Combine(directory, $"{stem}_p{i + 1}.png");
using var stream = File.Create(target);
encoder.Save(stream);
}
Console.WriteLine($"{shtCod}: 페이지 {designer.Pages.Count} 렌더 완료");
return 0;
}
catch (Exception ex)
{
File.WriteAllText(pngPath + ".err.txt", ex.ToString());
return 1;
}
}
}