재설계 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<string,bool> 이라 숫자를 못 담던 것이 원인이었다. 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>
122 lines
4.2 KiB
C#
122 lines
4.2 KiB
C#
using System.IO;
|
|
using System.Text.Json;
|
|
|
|
namespace SheetMe.Designer.Services;
|
|
|
|
/// <summary>
|
|
/// 사용자별 화면 취향 보존 — %LocalAppData%\SheetMe\prefs.json.
|
|
///
|
|
/// 업무 데이터가 아니라 "이 사람이 이 화면을 어떻게 보길 원하는가"만 담는다. 매 실행마다 사용자가
|
|
/// 같은 조작을 반복하게 만들지 않는 것이 목적이라, 읽기·쓰기 실패는 전부 삼키고 기본값으로 간다
|
|
/// (설정 파일 때문에 앱이 막히면 안 된다). <see cref="ThemeManager"/> 와 같은 폴더 규약.
|
|
/// </summary>
|
|
public static class UserPrefs
|
|
{
|
|
#region Member Fields
|
|
private static readonly string SettingsPath =
|
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "SheetMe", "prefs.json");
|
|
|
|
/// <summary>
|
|
/// 값 형식이 <see cref="JsonElement"/> 인 이유: 전에는 <c>Dictionary<string, bool></c> 였고
|
|
/// 그래서 숫자를 담을 수 없었다(창 크기·패널 폭을 저장하지 못한 원인이다).
|
|
/// 여기서 형식을 <c>string</c> 이나 <c>double</c> 로 바꾸면 이미 깔린 prefs.json 이
|
|
/// 역직렬화에서 터져 <b>기존 취향이 통째로 초기화된다</b>.
|
|
/// JsonElement 는 <c>false</c> 와 <c>640</c> 을 모두 그대로 받으므로 옛 파일이 살아남는다.
|
|
/// </summary>
|
|
private static Dictionary<string, JsonElement> values = new(StringComparer.Ordinal);
|
|
private static bool loaded;
|
|
#endregion
|
|
|
|
#region Methods
|
|
/// <summary>불리언 취향 읽기 — 미저장이면 기본값</summary>
|
|
public static bool GetBool(string key, bool fallback)
|
|
{
|
|
EnsureLoaded();
|
|
return values.TryGetValue(key, out var value)
|
|
&& value.ValueKind is JsonValueKind.True or JsonValueKind.False
|
|
? value.GetBoolean()
|
|
: fallback;
|
|
}
|
|
|
|
/// <summary>불리언 취향 저장</summary>
|
|
public static void SetBool(string key, bool value)
|
|
{
|
|
EnsureLoaded();
|
|
if (GetBool(key, !value) == value && values.ContainsKey(key))
|
|
{
|
|
return;
|
|
}
|
|
values[key] = JsonSerializer.SerializeToElement(value);
|
|
Save();
|
|
}
|
|
|
|
/// <summary>숫자 취향 읽기 — 미저장이거나 형식이 다르면 기본값</summary>
|
|
public static double GetDouble(string key, double fallback)
|
|
{
|
|
EnsureLoaded();
|
|
return values.TryGetValue(key, out var value)
|
|
&& value.ValueKind == JsonValueKind.Number
|
|
&& value.TryGetDouble(out var number)
|
|
&& double.IsFinite(number)
|
|
? number
|
|
: fallback;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 숫자 취향 여러 개를 한 번에 저장 — 창을 닫을 때 6개를 각각 저장하면 파일을 6번 쓴다.
|
|
/// </summary>
|
|
public static void SetDoubles(params (string Key, double Value)[] entries)
|
|
{
|
|
EnsureLoaded();
|
|
var changed = false;
|
|
foreach (var (key, value) in entries)
|
|
{
|
|
if (!double.IsFinite(value) || Math.Abs(GetDouble(key, double.NaN) - value) < 0.5)
|
|
{
|
|
continue;
|
|
}
|
|
values[key] = JsonSerializer.SerializeToElement(Math.Round(value, 1));
|
|
changed = true;
|
|
}
|
|
if (changed)
|
|
{
|
|
Save();
|
|
}
|
|
}
|
|
|
|
private static void EnsureLoaded()
|
|
{
|
|
if (loaded)
|
|
{
|
|
return;
|
|
}
|
|
loaded = true;
|
|
try
|
|
{
|
|
if (File.Exists(SettingsPath))
|
|
{
|
|
values = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(File.ReadAllText(SettingsPath))
|
|
?? new Dictionary<string, JsonElement>(StringComparer.Ordinal);
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// 손상된 설정은 무시하고 기본값으로 — 다음 저장 때 덮인다
|
|
}
|
|
}
|
|
|
|
private static void Save()
|
|
{
|
|
try
|
|
{
|
|
Directory.CreateDirectory(Path.GetDirectoryName(SettingsPath)!);
|
|
File.WriteAllText(SettingsPath, JsonSerializer.Serialize(values));
|
|
}
|
|
catch
|
|
{
|
|
// 쓰기 실패(권한·잠금)로 업무를 막지 않는다 — 이번 실행에만 반영된다
|
|
}
|
|
}
|
|
#endregion
|
|
}
|