메인 화면을 게이트에 넣고, 최소 크기를 단일 출처로, 창 크기를 기억한다

재설계 1단계 계속.

<b>메인 셸이 게이트에 아예 없었다.</b> 사용자가 하루 종일 보는 화면만 자동 검증 밖이었다.
넣고 나서 글자 31개만 검사되는 것을 보고 빈 껍데기를 찍고 있다는 것을 알았다 —
컨트롤을 놓고 하나를 골라 인스펙터·레이어를 채우니 81~95개가 된다.
세 크기·탭으로 찍는다: 기본 1440x920, 현장 최대화 1920x1032, 도구 상자 탭.
Loaded 커맨드(서식 목록 DB 조회)는 진단 모드에서 돌지 않게 막았다 —
게이트 결과가 접속 상태에 따라 달라지면 안 된다.

<b>게이트가 게이트의 오류를 잡았다.</b> 메인 화면을 넣자 세로잘림 30건이 떴는데 전부 오탐이었다.
DesiredSize 는 Margin 을 포함하고 RenderSize 는 포함하지 않아서, 세로 Margin 이 있는
TextBlock 이 예외 없이 걸린 것이다(도구 상자 그룹 머리가 "필요 27 > 실제 14" 였고 그 13 이 Margin).
Margin 을 빼고 비교하도록 고쳤다. 이 검사기에서 측정이 나를 정정한 세 번째다.

<b>--maxrect 가 최소 크기를 베껴 두고 있었다.</b> ProbeMinWidth=940 / ProbeMinHeight=640 /
ProbeBorder=6 은 MainView.xaml 의 복사본이라, 최소 크기를 바꾸면 이 검사가
<b>옛 값을 계속 검사하며 계속 통과한다</b>. 실제 MainView 를 만들어 XAML 이 정한 값을 읽는다.

<b>창 크기·위치·상태·두 패널 폭을 기억한다.</b> UserPrefs 가 Dictionary&lt;string,bool&gt; 이라
숫자를 못 담던 것이 원인이었다. JsonElement 로 바꿔 이미 깔린 prefs.json 이 살아남게 했다 —
string 이나 double 로 바꾸면 역직렬화가 터져 기존 취향이 통째로 초기화된다.

복원값은 반드시 클램프한다. 이 앱은 WindowStyle=None 이라 제목 표시줄을 직접 그리므로,
저장된 위치가 지금 화면에 없으면(회사에서 두 번째 모니터, 집에서 노트북) 메뉴·저장·닫기가
전부 손에 닿지 않고 마우스로 되돌릴 방법이 없다.
기본 크기로는 이 문제가 안 나지만, 저장을 시작하는 순간 복원되는 것은 사용자가 만든 값이다.
닫기를 물렀을 때는 저장하지 않는다 — 조작하지 않은 값이 남는다.

게이트: 테스트 277/277, --dialog-shots 대조군 4/4 · 글자 2,465개 검사 · 넘침 0건,
--maxrect 0실패(MainView 실측 940x640 테두리 6), --cleartype 11/11,
--edit-smoke 0실패, --modal-check 0실패, 빌드 경고 0,
--db-smoke 1271건 diff 0, --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Msystech
2026-08-14 16:50:24 +09:00
co-authored by Claude Opus 5
parent 862657cf2f
commit 4150ebe1d7
9 changed files with 270 additions and 17 deletions
@@ -0,0 +1,99 @@
using System.Windows;
namespace SheetMe.Designer.Services;
/// <summary>
/// 창 크기·위치·패널 폭을 실행 사이에 기억한다.
///
/// <b>왜 필요한가.</b> 전에는 아무것도 기억하지 않아서, 창을 옮기거나 패널 폭을 조절해도
/// 다음 실행에 원래대로 돌아갔다. 하루 종일 쓰는 사람에게 그 조작은 매일 반복하는 일이 된다.
///
/// <b>복원값을 반드시 클램프한다.</b> 저장된 사각형이 지금 화면에 없을 수 있다 —
/// 회사에서 두 번째 모니터를 쓰다 집에서 노트북만 켜면, 저장된 위치가 존재하지 않는 화면을 가리킨다.
/// 이 앱은 <c>WindowStyle="None"</c> 이라 OS 캡션이 없고 제목 표시줄을 직접 그리므로,
/// 창이 화면 위쪽으로 나가면 <b>메뉴·저장·닫기가 전부 손에 닿지 않는다</b>(마우스로 되돌릴 방법이 없다).
/// 그래서 복원 전에 작업영역과 교차 검사를 한다.
///
/// 현장 단말이 1920x1080 이상이라 기본 크기(1440x920)로는 이 문제가 나지 않는다.
/// 그래도 클램프를 두는 이유는, 저장을 시작하는 순간 <b>사용자가 만든 값</b>이 복원되기 때문이다 —
/// 기본값이 안전한 것과 저장된 값이 안전한 것은 다른 문제다.
/// </summary>
public static class WindowPlacement
{
#region Member Fields
private const string KeyLeft = "Window.Left";
private const string KeyTop = "Window.Top";
private const string KeyWidth = "Window.Width";
private const string KeyHeight = "Window.Height";
private const string KeyMaximized = "Window.Maximized";
private const string KeyLeftPanel = "Panel.Left.Width";
private const string KeyRightPanel = "Panel.Right.Width";
/// <summary>제목 표시줄을 잡을 수 있어야 하는 최소 노출 폭·높이</summary>
private const double Grip = 120;
#endregion
#region Methods
/// <summary>
/// 저장된 사각형을 복원한다 — 화면 밖이면 버리고 기본값을 쓴다.
/// <c>Show()</c> 전에 불러야 한다(보인 뒤에 옮기면 한 프레임 번쩍인다).
/// </summary>
public static void Restore(Window window)
{
var width = UserPrefs.GetDouble(KeyWidth, window.Width);
var height = UserPrefs.GetDouble(KeyHeight, window.Height);
var left = UserPrefs.GetDouble(KeyLeft, double.NaN);
var top = UserPrefs.GetDouble(KeyTop, double.NaN);
window.Width = Math.Max(window.MinWidth, width);
window.Height = Math.Max(window.MinHeight, height);
if (double.IsNaN(left) || double.IsNaN(top))
{
return;
}
var work = SystemParameters.WorkArea;
var rect = new Rect(left, top, window.Width, window.Height);
// 어느 화면에도 충분히 걸치지 않으면 저장값을 버린다. 여러 모니터를 정확히 다루려면
// 모니터 열거가 필요하지만, 주 화면 작업영역과의 교차만으로도 '손에 닿지 않는 창'은 막힌다.
if (rect.Right < work.Left + Grip || rect.Left > work.Right - Grip
|| rect.Bottom < work.Top + Grip || rect.Top > work.Bottom - Grip)
{
return;
}
window.WindowStartupLocation = WindowStartupLocation.Manual;
window.Left = left;
// 위로는 절대 넘기지 않는다 — 제목 표시줄이 화면 밖으로 나가면 되돌릴 수 없다
window.Top = Math.Max(work.Top, top);
if (UserPrefs.GetBool(KeyMaximized, false))
{
window.WindowState = WindowState.Maximized;
}
}
/// <summary>지금 상태를 저장한다 — 창을 닫을 때 한 번 부른다</summary>
public static void Save(Window window, double leftPanel, double rightPanel)
{
UserPrefs.SetBool(KeyMaximized, window.WindowState == WindowState.Maximized);
// 최대화 중이면 Left/Top/Width/Height 는 '복원했을 때의 값'이라 그대로 쓰는 것이 맞다.
// 최소화 중이면 그 값이 쓰레기이므로 건드리지 않는다.
if (window.WindowState == WindowState.Minimized)
{
return;
}
var bounds = window.WindowState == WindowState.Maximized
? window.RestoreBounds
: new Rect(window.Left, window.Top, window.ActualWidth, window.ActualHeight);
UserPrefs.SetDoubles(
(KeyLeft, bounds.Left), (KeyTop, bounds.Top),
(KeyWidth, bounds.Width), (KeyHeight, bounds.Height),
(KeyLeftPanel, leftPanel), (KeyRightPanel, rightPanel));
}
/// <summary>패널 폭 복원 — 0 이면 XAML 기본값을 그대로 둔다는 뜻</summary>
public static double LeftPanelWidth(double fallback) => UserPrefs.GetDouble(KeyLeftPanel, fallback);
public static double RightPanelWidth(double fallback) => UserPrefs.GetDouble(KeyRightPanel, fallback);
#endregion
}