Files
SheetMe/src/SheetMe.Designer/Diagnostics/DialogShots.cs
T
MsystechandClaude Opus 5 3ef1937902 '새 서식'을 없앤다 — 코드 없는 문서는 그린 뒤에 저장이 막힌다
서식은 항상 목록에서 골라 연다는 결정.

## 없앤 진입점 넷

- 파일 메뉴 '새 서식(N)'
- Ctrl+N
- 탭 흐름 옆 '+' 버튼(DesignerTheme.xaml)
- 빈 화면의 '새 서식' 버튼

시작 시 빈 문서를 만들던 것도 없앴다. 그게 실제 이유다 —
코드 없는 문서는 DB 에 저장할 수 없고(FormId 가 "NewSheet"), 사용자는
한참 그려 넣은 뒤에야 그 사실을 알게 된다. 만들 수 있게 두는 것 자체가 함정이었다.

이제 기동 인자에 서식 코드가 없으면 아무것도 열지 않고 서식 목록 탭을 띄운다.
빈 화면 안내도 '서식 목록에서 열기' 하나로 줄였다(F4 · Ctrl+O 병기).

CreateNew 와 파일 저장·열기 코드는 남긴다 — 접속 없이 도는 진단이 그 경로를 쓴다.
UI 진입점만 없앤 것이고, 이는 'DB에서 열기' 창을 지울 때와 같은 방식이다.
NewFileCommand 를 쓰던 --dialog-shots 는 AttachBlankForDiagnostics 로 바꿨다
(이름에 '진단 전용'을 박아 UI 에서 다시 부르지 못하게 했다).

## 없애자마자 드러난 것

문서가 아예 없는 상태가 <b>기본</b>이 되니 속성 패널에 정렬 바와 간격값이
그대로 남아 있었다. CurrentDesigner.Inspector 가 null 이면 안쪽 Visibility
바인딩이 전부 실패하고, WPF 는 <b>실패한 바인딩을 기본값(Visible)으로 떨군다</b> —
Collapsed 가 아니다. 선택이 없다는 안내와 정렬 바가 동시에 떠 있었다.

열린 서식이 없으면 인스펙터를 접는다. 그리고 이 회귀는 값으로 잡히지 않으므로
시각 게이트에 대조군을 넣었다 —
00-main-default(문서 있음 → 인스펙터 보임) 대 00d-main-empty(문서 없음 → 접힘).
대조군 없이 빈 화면만 찍으면 "원래 그랬던 것"과 구분되지 않는다.

## 게이트

- dotnet test 323/323
- --edit-smoke 실패 0
- --dialog-shots FAIL 0 (00d-main-empty 추가)
- --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 09:27:58 +09:00

526 lines
28 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.IO;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using SheetMe.Core.Models;
using SheetMe.Data.Stores;
using SheetMe.Designer.Services;
using SheetMe.Designer.ViewModels;
using SheetMe.Designer.ViewModels.Inspector;
namespace SheetMe.Designer.Diagnostics;
/// <summary>
/// 별도 창(대화상자) 오프스크린 스냅샷 — <c>--dialog-shots &lt;출력폴더&gt;</c>.
///
/// 창 10종을 다크·라이트 양 테마로 열어 PNG 로 남긴다. 목적은 하나다:
/// <b>테마 대비 결함을 사람 손 없이 재현 가능하게 확인한다.</b>
/// 실입력(마우스·키보드) 주입은 화면 잠금·세션 격리 상태에서 OS 가 거부하지만
/// 이 경로는 창을 화면 밖에 띄워 RenderTargetBitmap 으로 찍으므로 그 제약을 받지 않는다.
///
/// 창은 화면 밖(-10000)에 <c>Show()</c> 로 띄운다 — 레이아웃이 실제로 돌아야 템플릿·트리거가
/// 적용된 상태로 찍힌다(Measure/Arrange 만으로는 Window 크롬이 구성되지 않는다).
/// 모든 창은 찍은 뒤 즉시 닫는다.
/// </summary>
public static class DialogShots
{
#region Methods
public static int Run(string outputDirectory)
{
try
{
Directory.CreateDirectory(outputDirectory);
// 기본 ShutdownMode 는 OnLastWindowClose 라, 찍고 닫는 순간 마지막 창이 사라져 앱이 종료된다
// (그 뒤 Application.Current 가 null 이 되어 테마 교체에서 NRE). 명시 종료로 바꾼다.
Application.Current.ShutdownMode = ShutdownMode.OnExplicitShutdown;
var lines = new List<string>();
foreach (var light in new[] { false, true })
{
ThemeManager.Apply(light);
var suffix = light ? "light" : "dark";
foreach (var (name, factory) in Factories())
{
try
{
var window = factory();
Capture(window, Path.Combine(outputDirectory, $"{name}-{suffix}.png"));
// 창 배경이 실제로 무엇으로 해석됐는지 함께 남긴다 — 픽셀만 보면 원인을 못 가린다.
// WPF 는 암시 스타일을 요소의 '정확한 타입'으로만 찾으므로, Window 를 상속한
// 대화상자에는 <Style TargetType="Window"> 가 적용되지 않는다(기본 흰 배경이 된다).
var background = window.Background is SolidColorBrush solid
? solid.Color.ToString()
: window.Background?.ToString() ?? "(null)";
var styled = window.Style is not null ? "스타일적용" : "스타일없음";
lines.Add($"OK {name}-{suffix} 배경={background} {styled}");
// 그림은 증거만 만든다 — 잘렸는지는 값으로 단정해야 CI 가 잡는다
var violations = OverflowCheck.Inspect(window, out var inspected);
lines.Add($" 글자 {inspected}개 검사, 넘침 {violations.Count}건");
foreach (var violation in violations)
{
lines.Add($"FAIL 넘침 {name}-{suffix} {violation}");
}
}
catch (Exception ex)
{
lines.Add($"FAIL {name}-{suffix} — {ex.GetType().Name}: {ex.Message}");
}
}
}
lines.Add(string.Empty);
lines.AddRange(StyleSelfCheck());
lines.Add(string.Empty);
lines.AddRange(OverflowCheck.SelfCheck());
File.WriteAllText(Path.Combine(outputDirectory, "_report.txt"),
string.Join(Environment.NewLine, lines));
Console.WriteLine(string.Join(Environment.NewLine, lines));
return lines.Any(l => l.StartsWith("FAIL", StringComparison.Ordinal)) ? 1 : 0;
}
catch (Exception ex)
{
Console.Error.WriteLine($"대화상자 스냅샷 실패: {ex}");
return 1;
}
}
/// <summary>
/// 암시 스타일 자가 점검 — 스크린샷에 안 잡히는 표면(툴팁·포커스링·툴바 자식·스크롤 코너)이
/// 실제로 테마 스타일을 받는지 값으로 확인한다.
/// 이들은 팝업이거나 프레임워크 템플릿 소속이라 창을 찍어도 보이지 않는데, 스타일이 없으면
/// WPF 기본(Aero2) 밝은 크롬으로 떨어져 다크에서 글자가 사라진다.
/// </summary>
private static List<string> StyleSelfCheck()
{
var lines = new List<string> { "[암시 스타일 자가 점검 — 라이트 테마 기준]" };
// 떼어 놓은 인스턴스는 트리에 붙기 전이라 Style 이 아직 null 이다 — 사전에 있는지로 판정한다
foreach (var (label, key) in new (string, object)[]
{
("ToolTip", typeof(System.Windows.Controls.ToolTip)),
("ListView", typeof(System.Windows.Controls.ListView)),
("ToolBar", typeof(System.Windows.Controls.ToolBar)),
("ToolBar.Button", System.Windows.Controls.ToolBar.ButtonStyleKey),
("ToolBar.Separator", System.Windows.Controls.ToolBar.SeparatorStyleKey),
("ToolBar.TextBox", System.Windows.Controls.ToolBar.TextBoxStyleKey),
("FocusVisual", SystemParameters.FocusVisualStyleKey),
("ScrollViewer코너", SystemColors.ControlBrushKey),
})
{
var found = Application.Current.TryFindResource(key);
lines.Add($" {label,-18} {(found is null ? " 미정의(Aero2 폴백)" : "정의됨")}");
}
return lines;
}
/// <summary>창 생성기 목록 — 실제 사용 시와 같은 인자로 만든다(빈 껍데기를 찍으면 의미가 없다)</summary>
private static List<(string Name, Func<Window> Factory)> Factories()
{
var designer = SampleDesigner();
return new List<(string, Func<Window>)>
{
// 메인 셸 — 이 게이트에 <b>아예 없었다</b>. 사용자가 하루 종일 보는 화면만 자동 검증 밖이었다.
// 두 크기로 찍는다: 기본 창(1440×920)과 현장 단말 최대화 크기(1920×1032).
// 현장이 1920×1080 이상뿐이라는 답을 받았으므로 그 사각형이 실제 기준이다.
("00-main-default", () => NewMainShell(1440, 920)),
("00b-main-1920", () => NewMainShell(1920, 1032)),
// 도구 상자 탭은 타일 라벨이 좁은 2열에 들어가므로 잘림이 가장 잘 나는 자리다
("00c-main-toolbox", () => NewMainShell(1440, 920, leftTab: 2)),
// 'DB에서 열기' 대화상자를 지웠다. 그 자리를 좌측 서식 목록 샷이 대신한다 —
// 안 그러면 서식명 잘림을 잡을 자리가 게이트에서 통째로 사라진다.
("01-sheet-list", () => NewMainShell(1440, 920, leftTab: 0, sheets: SampleSheets())),
// 문서 없는 시작 화면 — '새 서식'을 없앤 뒤로는 이것이 <b>기본 상태</b>다.
// 여기가 대조군이다: 00-main-default 는 문서가 있어 인스펙터가 보여야 하고,
// 이 샷은 없어서 접혀야 한다. 접히지 않으면 DataContext 가 null 인 채로
// 안쪽 Visibility 바인딩이 전부 실패해 정렬 바·간격값이 남는다(WPF 는 실패를 Visible 로 떨군다).
("00d-main-empty", () => NewMainShell(1440, 920, leftTab: 0,
sheets: SampleSheets(), withDocument: false)),
("02-sheet-history", () => new Views.SheetHistoryDialogView("S999", "표본 서식", SampleVersions())),
("03-query-editor", () => NewQueryEditor(designer,
"SELECT ChtNum, PatNam FROM P_PatMst\nWHERE ChtNum = <<M.CMM.HISOperatingInfo.bzPatientInfo.ChtNum>>")),
("03b-query-trial", () => NewQueryEditorWithTrial(designer)),
// 한글이 섞인 쿼리 — 색칠 레이어와 줄번호가 한글에서 어긋나는지 눈으로 확인할 경로
("03c-query-hangul", () => NewQueryEditor(designer,
"select 이름, 주소 from 환자정보\nwhere 상태 = '입원'\n-- 세 번째 줄 주석\nand 나이 >= 20")),
// 실제 카탈로그(384종)와 실제 제안 순위로 찍는다 — 표본 몇 개로는 분류·건수가 안 보인다
("04-tag-picker", () => new Views.TagPickerDialogView(
"데이터 태그 선택 — 자동 채움 원천(bzDataInterface)",
Core.Catalog.LegacyTagCatalog.DataInterfaceTags, null,
Core.Catalog.LegacyTagUsageCatalog.SuggestFor("DataInterfaceTag", "TextBox"))),
("05-mask-picker", () => new Views.MaskPickerDialogView("0000년 90월 90일")),
// 데이터소스 배선 — 운영 다수파인 Rows 형 값을 넣어 되읽기·형태 유지가 보이게 찍는다
("05b-datatable-field", () => new Views.DataTableFieldDialogView(
"MDataTable2.Rows(0).Item(\"ALGYON\")", new[] { "MDataTable1", "MDataTable2" })),
("06-font-manager", () => new Views.FontManagerDialogView(designer)),
("07-register-sheet", () => new Views.RegisterSheetDialogView("S999", "표본 서식")),
("08-preview", () => new Views.PreviewWindow(designer)),
// 환자 선택 — 목록 둘이 빈 상태로 찍힌다(진단은 DB 없이도 같은 답을 내야 한다).
// 빈 상태의 레이아웃도 회귀 대상이다: 목록이 비면 창이 무너지는 배치가 흔하다.
("08b-patient-picker", () => new Views.PatientPickerDialogView()),
// 데이터소스 결과 — 실패한 상태로 찍는다. 성공 화면만 회귀 대상으로 두면
// 정작 사람이 오래 들여다보는(= 값이 안 나올 때) 화면이 검사에서 빠진다.
("08c-datasource-result", () => new Views.DataSourceResultDialogView(SampleRunner())),
// 인스펙터는 UserControl 이라 창이 없다 — 실폭 300px 호스트 창에 담아 실제 배치를 찍는다.
// 속성 패널 레이아웃 회귀를 눈으로 확인할 수 있는 유일한 자동 경로다.
("09-inspector-label", () => HostInspector(designer, "Label1")),
("10-inspector-textbox", () => HostInspector(designer, "TextBox1")),
("11-inspector-data", () => HostInspector(designer, "TextBox1", InspectorTab.Data)),
("12-inspector-behavior", () => HostInspector(designer, "TextBox1", InspectorTab.Behavior)),
// 태그 제안이 타입별로 좁혀지는지 — 이미지는 서명·직인 6종만 떠야 한다
("12b-inspector-picture-data", () => HostInspector(designer, "MPictureBox1", InspectorTab.Data)),
// 정렬 바는 선택 상태에 따라 나타나야 한다 — 선택 0 과 페이지 속성 모드에서는 안 보여야 하는데
// 그건 수치로 못 잡는다(HasSelection 을 조건으로 쓰면 페이지 모드에서 뜬다).
// 15/16 이 같은 designer 에 GroupSelection 을 걸므로 반드시 그 앞에 둔다.
("12c-inspector-none", () => HostInspectorSelection(designer, InspectorShotMode.None)),
("12d-inspector-multi", () => HostInspectorSelection(designer, InspectorShotMode.Multi)),
("12e-inspector-page", () => HostInspectorSelection(designer, InspectorShotMode.Page)),
("13-layer-type-filter", () => new Views.LayerTypeFilterDialog(
designer.LayerTypeCandidates(), new[] { "Label" })),
// 레이어 아웃라인 — 선택 없음 / 컨트롤 하나 선택 / 그룹 전체 선택 세 상태를 각각 찍는다.
// 강조는 IsSelected 가 아니라 Highlight 3상태가 그리므로 눈으로 확인할 경로가 이것뿐이다.
("14-layer-plain", () => HostLayerPanel(designer, LayerShotMode.None)),
("15-layer-selected", () => HostLayerPanel(designer, LayerShotMode.Single)),
("16-layer-group", () => HostLayerPanel(designer, LayerShotMode.Group)),
// 알림 대화상자 — 성격 4종과 버튼 3종의 대표 조합. 순정 MessageBox 를 걷어낸 자리라
// 라이트/다크 회귀를 여기서 잡는다.
("17-msg-error", () => new Views.MessageDialogView(
Services.DialogKind.Error, "오류", "DB 저장 중 오류가 발생했습니다. (트랜잭션 롤백됨)",
"ORA-00001: unique constraint (MSYSTECHHIS.PK_E_SDGMST) violated\n오류 코드: 20260813-0007",
Services.DialogButtons.Ok)),
("18-msg-warning", () => new Views.MessageDialogView(
Services.DialogKind.Warning, "DB 저장", "DB 저장이 비활성화되어 있습니다.",
"appsettings.json 의 FormStore:SaveMode 를 'Db' 로 설정한 뒤 사용하세요.\n" +
"(운영 레거시 테이블에 기록되므로 명시적 활성화가 필요합니다)",
Services.DialogButtons.Ok)),
("19-msg-confirm", () => new Views.MessageDialogView(
Services.DialogKind.Question, "페이지 삭제", "페이지 2을(를) 삭제할까요?",
"컨트롤 116개가 함께 삭제됩니다.",
Services.DialogButtons.YesNo, yes: "삭제", no: "취소", destructive: true)),
("20-msg-save", () => new Views.MessageDialogView(
Services.DialogKind.Question, "종료", "'P093 간호초기평가기록지_V2' 문서에 저장하지 않은 변경이 있습니다.",
"저장할까요?", Services.DialogButtons.YesNoCancel, yes: "저장", no: "저장 안 함")),
("21-msg-info", () => new Views.MessageDialogView(
Services.DialogKind.Info, "DB 저장", "저장되었습니다. (SdgKey 52426)",
"레거시 뷰어/디자이너에서 열어 확인하세요.", Services.DialogButtons.Ok)),
};
}
/// <summary>레이어 패널 스냅숏의 선택 상태</summary>
private enum LayerShotMode
{
/// <summary>선택 없음</summary>
None,
/// <summary>컨트롤 하나 선택 — 파란 밴드 하나</summary>
Single,
/// <summary>그룹 전체 선택 — 폴더 파랑 + 멤버 서브트리, 이어진 블록</summary>
Group,
}
/// <summary>
/// 레이어 패널을 실제 패널 폭(255px)의 창에 담는다.
/// MainViewModel 을 통째로 만들면 DB 조회가 딸려 오므로, 패널이 읽는 <c>CurrentDesigner</c> 하나만
/// 들고 있는 최소 DataContext 를 세워 준다.
/// </summary>
/// <summary>
/// 메인 셸을 주어진 크기로 만든다.
///
/// Loaded 커맨드(서식 목록 DB 조회)는 <see cref="StartupArguments.DiagnosticMode"/> 때문에 돌지 않으므로
/// 접속 없이도 같은 결과가 나온다. 문서 한 장을 넣어 캔버스·레이어·인스펙터가 빈 상태가 아니게 한다 —
/// 빈 껍데기를 찍으면 잘림을 볼 수 없다.
/// </summary>
private static Window NewMainShell(double width, double height, int leftTab = 1,
List<SheetSummary>? sheets = null, bool withDocument = true)
{
var window = new Views.MainView
{
Width = width,
Height = height,
Left = -10000,
Top = -10000,
WindowStartupLocation = WindowStartupLocation.Manual,
};
if (window.DataContext is not MainViewModel shell)
{
return window;
}
if (withDocument)
{
shell.AttachBlankForDiagnostics();
}
shell.SelectedLeftTabIndex = leftTab;
// 진단 모드는 Loaded 커맨드를 안 돌리므로 목록이 비어 있다 —
// 빈 목록을 찍으면 서식명 잘림을 볼 수 없다. 표본을 직접 넣는다.
if (sheets is not null)
{
shell.SheetList.Clear();
foreach (var sheet in sheets)
{
shell.SheetList.Add(sheet);
}
}
// 컨트롤을 놓고 하나를 고른다 — 이걸 안 하면 인스펙터가 비어 있고 레이어에 행이 없어서
// 검사할 글자가 31개밖에 안 나온다(빈 껍데기를 찍으면 잘림을 볼 수 없다).
// 이름이 긴 컨트롤을 섞는다 — 좁은 칸에서 잘리는 것은 짧은 이름이 아니라 긴 이름이다.
if (shell.CurrentDesigner is { } designer)
{
foreach (var type in new[] { "Label", "TextBox", "CheckBox", "MDataTable", "MPictureBox" })
{
designer.AddPaletteItemAtCenter(type);
}
foreach (var control in designer.Pages[0].Controls)
{
if (control.Type == "Label")
{
control.Model.Id = "아주긴이름을가진라벨컨트롤_LAB_00123";
control.Model.Props.SetText("Text",
"잘림을 보려면 충분히 긴 문구가 필요하다 — 간호·간병통합서비스 병동");
control.NotifyAllChanged();
break;
}
}
if (designer.Pages[0].Controls.FirstOrDefault() is { } first)
{
designer.Selection.SetSingle(first);
}
}
return window;
}
private static Window HostLayerPanel(DesignerViewModel designer, LayerShotMode mode)
{
designer.Selection.Clear();
designer.SetLayerTypeFilter(Array.Empty<string>());
designer.LayerFilter = string.Empty;
var page = designer.Pages[0];
if (mode != LayerShotMode.None && page.Controls.Count >= 3)
{
designer.Selection.Set(new[] { page.Controls[^1], page.Controls[^2] });
designer.GroupSelection();
if (mode == LayerShotMode.Single)
{
designer.Selection.SetSingle(page.Controls[0]);
}
}
designer.RebuildLayerRows();
return new Window
{
Title = $"레이어 — {mode}",
Width = 255,
Height = 620,
Content = new Views.LayerPanelView { DataContext = new LayerShotContext(designer) },
Background = Application.Current.TryFindResource("B.Panel") as Brush,
};
}
/// <summary>패널이 바인딩하는 <c>CurrentDesigner</c> 만 노출하는 스냅숏 전용 DataContext</summary>
private sealed class LayerShotContext
{
public LayerShotContext(DesignerViewModel designer) => CurrentDesigner = designer;
public DesignerViewModel CurrentDesigner { get; }
}
/// <summary>인스펙터를 실제 패널 폭(300px)의 창에 담는다 — 컨트롤 하나를 선택한 상태로</summary>
private static Window HostInspector(DesignerViewModel designer, string controlId,
InspectorTab tab = InspectorTab.Design)
{
var page = designer.Pages[0];
var target = page.Controls.FirstOrDefault(c => c.Id == controlId) ?? page.Controls[0];
designer.Selection.SetSingle(target);
designer.Inspector.SelectedTab = tab;
designer.Inspector.Rebuild();
return new Window
{
Title = $"속성 — {controlId}",
Width = 300,
Height = 720,
Content = new Views.InspectorView { DataContext = designer.Inspector },
Background = Application.Current.TryFindResource("B.Panel") as Brush,
};
}
/// <summary>정렬 바가 선택 상태에 따라 나타나고 사라지는지 — 그림으로만 확인되는 것들</summary>
private enum InspectorShotMode { None, Multi, Page }
private static Window HostInspectorSelection(DesignerViewModel designer, InspectorShotMode mode)
{
var page = designer.Pages[0];
switch (mode)
{
case InspectorShotMode.Multi:
designer.Selection.Set(page.Controls.Take(3).ToList(), page.Controls[0]);
break;
case InspectorShotMode.Page:
designer.Selection.Clear();
designer.ActivatePage(page);
break;
default:
designer.Selection.Clear();
break;
}
designer.Inspector.SelectedTab = InspectorTab.Design;
designer.Inspector.Rebuild();
return new Window
{
Title = $"속성 — {mode}",
Width = 300,
Height = 720,
Content = new Views.InspectorView { DataContext = designer.Inspector },
Background = Application.Current.TryFindResource("B.Panel") as Brush,
};
}
/// <summary>화면 밖에 띄워 레이아웃을 돌린 뒤 PNG 로 찍고 닫는다</summary>
private static void Capture(Window window, string pngPath)
{
window.WindowStartupLocation = WindowStartupLocation.Manual;
window.Left = -10000;
window.Top = -10000;
window.ShowInTaskbar = false;
window.Show();
window.UpdateLayout();
// 레이아웃·비동기 로딩(Loaded 핸들러)이 한 바퀴 돌게 한다
Pump();
// 창 자체를 렌더한다 — Content 만 찍으면 Window.Background(B.AppBg)가 빠져 투명이 되고,
// PNG 에서 검정으로 저장돼 "다크처럼 보이는" 착시가 생긴다(라이트 결함이 가려진다).
var content = (FrameworkElement)window.Content;
var width = (int)Math.Ceiling(content.ActualWidth > 0 ? content.ActualWidth : window.Width);
var height = (int)Math.Ceiling(content.ActualHeight > 0 ? content.ActualHeight : window.Height);
var bitmap = new RenderTargetBitmap(Math.Max(1, width), Math.Max(1, height), 96, 96, PixelFormats.Pbgra32);
var drawing = new DrawingVisual();
using (var context = drawing.RenderOpen())
{
context.DrawRectangle(window.Background ?? Brushes.Transparent, null, new Rect(0, 0, width, height));
context.DrawRectangle(new VisualBrush(content) { Stretch = Stretch.None, AlignmentX = AlignmentX.Left, AlignmentY = AlignmentY.Top },
null, new Rect(0, 0, width, height));
}
bitmap.Render(drawing);
var encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bitmap));
using (var stream = File.Create(pngPath))
{
encoder.Save(stream);
}
window.Close();
}
/// <summary>디스패처 큐를 비운다(Loaded/바인딩 갱신 반영)</summary>
private static void Pump()
{
for (var i = 0; i < 3; i++)
{
System.Windows.Threading.Dispatcher.CurrentDispatcher.Invoke(
() => { }, System.Windows.Threading.DispatcherPriority.ContextIdle);
}
}
#endregion
#region Methods - 표본 데이터
private static List<SheetSummary> SampleSheets() => new()
{
new SheetSummary("C020", "협진기록지", true, true, "A", "의사기록"),
new SheetSummary("C030", "Doctor's Order List", false, true, "A", "의사기록"),
new SheetSummary("P001", "간호 기록지", true, true, "E", "간호기록"),
new SheetSummary("F004", "임상생리신경검사", true, false, "D", "검사결과"),
new SheetSummary("Z001", "분류 없는 서식", false, true, string.Empty, "(미분류)"),
};
/// <summary>
/// 데이터소스 표본 — 환자 없이 만든다. 그러면 치환이 전부 빈 값이 되어 SQL 이 깨지고,
/// 진단이 <b>실패 화면</b>을 찍는다. 그게 사람이 실제로 오래 보게 되는 화면이다.
/// DB 접속 여부와 무관하게 같은 그림이 나온다(치환 단계에서 이미 실패한다).
/// </summary>
private static Services.MDataTableRunner SampleRunner()
{
var document = new SheetMe.Core.Models.FormDocument();
var page = new SheetMe.Core.Models.FormPage { Width = 720, Height = 856 };
document.Pages.Add(page);
foreach (var (id, query) in new[]
{
("MDataTable1", "SELECT * FROM P_ComInf WHERE ComNum = <<bzPatientInfo.ComNum>>"),
("MDataTable2", "SELECT 1 FROM DUAL"),
})
{
var source = new SheetMe.Core.Models.ControlElement
{
Type = "MDataTable", Id = id,
Bounds = new SheetMe.Core.Models.LayoutRect { X = 0, Y = 0, W = 20, H = 20 },
};
source.Props.SetText("Query", query);
page.Controls.Add(source);
}
return new Services.MDataTableRunner(document, null);
}
private static List<DesignVersionInfo> SampleVersions() => new()
{
new DesignVersionInfo(52443, "202608121030", "011825", false),
new DesignVersionInfo(52380, "202607161422", "011825", true),
new DesignVersionInfo(52241, "202607160901", "MSYS", true),
};
private static string[] SampleTags() => new[]
{
"SetPatientName", "SetChartNumber", "EnableControl", "SetVisitDate", "ClearValue",
};
/// <summary>폰트 관리자·미리보기용 표본 문서 — 컨트롤이 있어야 목록이 비지 않는다</summary>
/// <summary>쿼리 편집기 — 이 서식의 컨트롤을 《컨트롤명》 후보로 넘겨 그룹이 실제로 차는지 본다</summary>
private static Window NewQueryEditor(DesignerViewModel designer, string sql)
=> new Views.QueryEditorWindow("데이터소스", sql, controlNames: designer.ControlNames());
/// <summary>
/// 검증 실행까지 마친 상태 — 결과 표가 실제로 채워지는지 눈으로 확인할 유일한 경로다.
/// DB 가 없으면 실패 메시지가 찍히고, 그것도 봐야 하는 화면이다.
/// </summary>
private static Window NewQueryEditorWithTrial(DesignerViewModel designer)
{
var window = new Views.QueryEditorWindow("데이터소스",
"SELECT ShtCod, ShtKorNam, ShtTyp FROM E_ShtMst WHERE ROWNUM <= 5",
controlNames: designer.ControlNames());
window.RunTrialForSmoke();
return window;
}
private static DesignerViewModel SampleDesigner()
{
var document = new FormDocument { FormId = "S999", Title = "표본 서식" };
var page = Core.Serialization.LegacyXmlSerializer.CreateEmptyPage(1);
page.Controls.Add(NewControl("Label", "Label1", "환자명", 40, 40, 120, 24));
page.Controls.Add(NewControl("TextBox", "TextBox1", string.Empty, 170, 40, 200, 26));
page.Controls.Add(NewControl("Label", "Label2", "생년월일", 40, 80, 120, 24));
// 이름만 있고 문구가 없는 컨트롤 — 레이어 목록에서 이름 칸이 중복 표시되지 않는지 확인용
page.Controls.Add(NewControl("TextBox", "TextBox2", string.Empty, 170, 80, 200, 26));
// 이미지는 데이터 태그 후보가 운영 전체에서 7종뿐이다 — 타입별 제안이 실제로 좁아지는지 확인용
page.Controls.Add(NewControl("PictureBox", "MPictureBox1", string.Empty, 400, 40, 120, 90));
document.Pages.Add(page);
return new DesignerViewModel(document);
}
private static ControlElement NewControl(string type, string id, string text, double x, double y, double w, double h)
{
var element = new ControlElement { Type = type, Id = id };
element.Bounds.X = x;
element.Bounds.Y = y;
element.Bounds.W = w;
element.Bounds.H = h;
if (text.Length > 0)
{
element.Props.SetText("Text", text);
}
return element;
}
#endregion
}