서식은 항상 목록에서 골라 연다는 결정. ## 없앤 진입점 넷 - 파일 메뉴 '새 서식(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>
526 lines
28 KiB
C#
526 lines
28 KiB
C#
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 <출력폴더></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
|
||
}
|