환자 선택을 제목줄로 올린다 — 한 번 고르면 앱 전체가 그 환자를 본다
테마 버튼 오른쪽에 user 아이콘. 전에는 미리보기 창 안에만 있어서 쿼리 편집처럼 환자 값이 필요한 다른 화면에서는 고를 방법이 없었다. ## 방향을 뒤집었다 전에는 미리보기 창이 환자를 들고 있고 세션에 <b>밀어 올렸다</b>. 그러면 제목줄에서 바꿀 때 창이 모르고, 세션과 창이 어긋나는 경로가 생긴다. 이제 모든 진입점이 PatientSession 에만 쓰고, 창들은 Changed 를 <b>듣는다</b>. 제목줄·미리보기·쿼리 편집 어디서 바꿔도 나머지가 따라온다. 미리보기 창은 열 때 세션의 환자를 물려받는다 — 창을 열 때마다 초기화하면 "앱 전체 적용"이 거짓이 된다. ## 아이콘이라 상태를 색으로 알린다 늘 보이는 자리라 글자를 둘 폭이 없다. 고르면 강조색, 이름은 툴팁에 둔다 — 제목줄에 환자 이름을 상시 노출하지 않는다. 이미 고른 상태에서 누르면 <b>메뉴</b>가 뜬다(바꾸기 / 해제). "누르면 해제"로 두면 늘 보이는 아이콘이라 실수로 지운다. ## 말뿐인지 값인지 확인했다 제목줄 아이콘은 세션만 건드린다. 열려 있는 창이 그것을 듣지 않으면 아이콘은 켜지는데 종이는 그대로다 — "설정했는데 안 먹는다"가 된다. 그래서 판정은 <b>창의 메서드를 부르지 않고</b> 세션에만 써서 종이가 따라오는지 본다. 구독을 떼는지도 본다. 정적 이벤트라 안 떼면 창이 회수되지 않고 죽은 창이 계속 그리려 든다 — 닫은 뒤 세션을 흔들어 예외 없이 지나가는지 확인한다. ## 게이트 - dotnet test 323/323 - --edit-smoke 실패 0 (전체 적용 판정 3건 신규) - --dialog-shots FAIL 0 · --query-popup 실패 0 - --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
1baf805f02
commit
5ecc7b514a
@@ -705,7 +705,32 @@ public static class EditSmoke
|
||||
!previewCleared.Contains("C-9003", StringComparison.Ordinal)
|
||||
&& previewCleared.Contains("[PAT_차트번호", StringComparison.Ordinal),
|
||||
previewCleared);
|
||||
|
||||
// ── "앱 전체 적용" 이 말뿐인지 값인지 ──
|
||||
// 제목줄 아이콘은 PatientSession 만 건드린다. 열려 있는 창들이 그것을 <b>듣지 않으면</b>
|
||||
// 아이콘은 켜지는데 종이는 그대로다 — 사용자에게는 "설정했는데 안 먹는다"가 된다.
|
||||
// 세션에만 쓰고 종이가 따라오는지 본다(창의 메서드를 직접 부르지 않는다).
|
||||
Services.PatientSession.Set(Fake("C-9004", "세션환자"), "표본");
|
||||
var afterSession = PreviewPaper();
|
||||
Check("전체 적용: 세션에 넣으면 열린 미리보기가 따라온다",
|
||||
afterSession.Contains("C-9004", StringComparison.Ordinal)
|
||||
&& afterSession.Contains("세션환자", StringComparison.Ordinal),
|
||||
afterSession);
|
||||
|
||||
Services.PatientSession.Set(null, string.Empty);
|
||||
var afterSessionClear = PreviewPaper();
|
||||
Check("전체 적용: 세션에서 떼면 열린 미리보기도 따라 뗀다",
|
||||
!afterSessionClear.Contains("C-9004", StringComparison.Ordinal)
|
||||
&& afterSessionClear.Contains("[PAT_차트번호", StringComparison.Ordinal),
|
||||
afterSessionClear);
|
||||
|
||||
// 창을 닫은 뒤에도 이벤트가 남아 있으면 회수되지 않고, 죽은 창이 계속 그린다.
|
||||
// 닫고 다시 세션을 건드려 <b>예외 없이</b> 지나가는지 본다(구독이 남아 있으면
|
||||
// 죽은 창의 PagesHost 를 만지다 터진다).
|
||||
patientPreview.Close();
|
||||
Services.PatientSession.Set(Fake("C-9005", "닫힌뒤"), "표본");
|
||||
Services.PatientSession.Set(null, string.Empty);
|
||||
Check("전체 적용: 창을 닫으면 구독이 떨어진다", true, "예외 없이 통과");
|
||||
|
||||
var emptyName = PatientPaper(
|
||||
new Services.PatientTagResolver(Fake("C-9002", string.Empty), new StubTagResolver()));
|
||||
|
||||
@@ -294,6 +294,14 @@
|
||||
<Button Style="{StaticResource Subtle}" Width="34" Height="28" Padding="0" ToolTip="다크/라이트 전환"
|
||||
Click="OnThemeToggleClick"
|
||||
Content="{Binding Source=sun, Converter={StaticResource IconName}, ConverterParameter=16}"/>
|
||||
<!--
|
||||
환자 선택 — 앱 전체에 적용된다. 전에는 미리보기 창 안에만 있어서
|
||||
쿼리 편집처럼 환자 값이 필요한 다른 화면에서 고를 수가 없었다.
|
||||
여기서 한 번 고르면 PatientSession 하나를 모든 화면이 본다.
|
||||
고른 상태는 <b>아이콘 색</b>으로 알린다 — 늘 보이는 자리라 글자를 둘 폭이 없다.
|
||||
-->
|
||||
<Button x:Name="PatientIconButton" Style="{StaticResource Subtle}"
|
||||
Width="34" Height="28" Padding="0" Click="OnPickPatientClick"/>
|
||||
<Button Style="{StaticResource Subtle}" Width="34" Height="28" Padding="0" ToolTip="DB에 저장"
|
||||
Command="{Binding SaveToDbCommand}"
|
||||
Content="{Binding Source=save, Converter={StaticResource IconName}, ConverterParameter=16}"/>
|
||||
|
||||
@@ -14,6 +14,12 @@ public partial class MainView : Window
|
||||
public MainView()
|
||||
{
|
||||
InitializeComponent();
|
||||
// 환자는 앱 전체가 하나를 공유한다 — 어느 화면에서 바뀌어도 이 아이콘이 따라와야 한다.
|
||||
// 창이 살아 있는 동안만 듣는다(정적 이벤트라 떼지 않으면 창이 회수되지 않는다).
|
||||
RefreshPatientIcon();
|
||||
PatientSession.Changed += OnPatientSessionChanged;
|
||||
Closed += (_, _) => PatientSession.Changed -= OnPatientSessionChanged;
|
||||
|
||||
// StaysOpen=False 팝업은 바깥 클릭(버튼 포함)으로 먼저 닫힌다 — 토글 버튼 재클릭이 곧바로 다시 열지 않게 닫힌 시각 기록
|
||||
AddControlPopup.Closed += (_, _) => addControlPopupClosedAt = Environment.TickCount;
|
||||
ZoomPopup.Closed += (_, _) => zoomPopupClosedAt = Environment.TickCount;
|
||||
@@ -73,6 +79,69 @@ public partial class MainView : Window
|
||||
ThemeMenuItem.IsChecked = !ThemeManager.IsLight;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 환자 선택 — 여기서 고르면 <b>앱 전체</b>가 그 환자를 본다.
|
||||
///
|
||||
/// 안 골랐으면 바로 선택 창을 연다. 이미 골랐으면 <b>메뉴</b>를 띄운다 —
|
||||
/// 늘 보이는 아이콘이라 "누르면 해제"로 두면 실수로 지운다.
|
||||
/// 바꾸기·해제를 글자로 보여 주고 고르게 하는 쪽이 예측 가능하다.
|
||||
/// </summary>
|
||||
private void OnPickPatientClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!PatientSession.HasPatient)
|
||||
{
|
||||
Pick();
|
||||
return;
|
||||
}
|
||||
var menu = new System.Windows.Controls.ContextMenu
|
||||
{
|
||||
PlacementTarget = PatientIconButton,
|
||||
Placement = System.Windows.Controls.Primitives.PlacementMode.Bottom,
|
||||
};
|
||||
var header = new System.Windows.Controls.MenuItem
|
||||
{
|
||||
Header = PatientSession.Label,
|
||||
IsEnabled = false,
|
||||
};
|
||||
var change = new System.Windows.Controls.MenuItem { Header = "환자 바꾸기..." };
|
||||
change.Click += (_, _) => Pick();
|
||||
var clear = new System.Windows.Controls.MenuItem { Header = "환자 해제" };
|
||||
clear.Click += (_, _) => PatientSession.Set(null, string.Empty);
|
||||
menu.Items.Add(header);
|
||||
menu.Items.Add(new System.Windows.Controls.Separator());
|
||||
menu.Items.Add(change);
|
||||
menu.Items.Add(clear);
|
||||
menu.IsOpen = true;
|
||||
|
||||
void Pick()
|
||||
{
|
||||
// 권한 문·감사 기록·문맥 읽기는 전부 Pick 안에 있다 — 호출부마다 다시 확인하면 어긋난다
|
||||
if (PatientPickerDialogView.Pick(this, out var label) is { } picked)
|
||||
{
|
||||
PatientSession.Set(picked, label);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 아이콘이 지금 상태를 말한다 — 늘 보이는 자리라 글자를 둘 폭이 없어 <b>색</b>으로 알린다.
|
||||
/// 고른 환자 이름은 툴팁에 둔다(제목줄에 환자 이름을 상시 노출하지 않는다).
|
||||
/// </summary>
|
||||
private void OnPatientSessionChanged(object? sender, EventArgs e) => RefreshPatientIcon();
|
||||
|
||||
private void RefreshPatientIcon()
|
||||
{
|
||||
if (PatientIconButton is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var on = PatientSession.HasPatient;
|
||||
PatientIconButton.Content = Controls.LucideIcons.Icon("user", 16, on ? "B.Accent" : null);
|
||||
PatientIconButton.ToolTip = on
|
||||
? $"환자: {PatientSession.Label}\n누르면 바꾸기·해제"
|
||||
: "환자 선택 — 태그와 MDataTable 에 실제 값이 들어갑니다(앱 전체 적용)";
|
||||
}
|
||||
|
||||
/// <summary>플로팅 바 — 컨트롤 추가 플라이아웃 토글(닫힘 직후 재클릭 가드)</summary>
|
||||
private void OnAddControlFlyoutClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
|
||||
@@ -61,18 +61,22 @@ public partial class PreviewWindow : Window
|
||||
tags = session;
|
||||
InitializeComponent();
|
||||
Activated += (_, _) => BuildPages();
|
||||
// 환자는 앱 전체가 하나를 공유한다 — 제목줄 아이콘에서 바꿔도 이 창이 따라와야 한다
|
||||
PatientSession.Changed += OnPatientSessionChanged;
|
||||
Closed += (_, _) =>
|
||||
{
|
||||
Unsubscribe();
|
||||
PatientSession.Changed -= OnPatientSessionChanged;
|
||||
if (ReferenceEquals(open, this))
|
||||
{
|
||||
open = null;
|
||||
}
|
||||
};
|
||||
Subscribe();
|
||||
// 환자 없이도 러너를 만든다 — 데이터소스가 몇 개 있는지는 지금 알 수 있어야 하고,
|
||||
// 그래야 '데이터소스' 버튼이 처음부터 맞는 상태로 뜬다.
|
||||
UsePatient(null, string.Empty);
|
||||
// 이미 다른 화면에서 고른 환자가 있으면 그것을 그대로 물려받는다 —
|
||||
// 창을 열 때마다 초기화하면 "앱 전체 적용"이 거짓이 된다.
|
||||
// 환자가 없어도 러너는 만든다(데이터소스 개수를 알아야 버튼 상태가 맞다).
|
||||
UsePatient(PatientSession.Current, PatientSession.Label);
|
||||
}
|
||||
#endregion
|
||||
|
||||
@@ -190,11 +194,18 @@ public partial class PreviewWindow : Window
|
||||
{
|
||||
return;
|
||||
}
|
||||
UsePatient(picked, label);
|
||||
// 세션에만 쓴다 — 그러면 Changed 가 이 창을 포함한 <b>모든</b> 화면을 갱신한다.
|
||||
// 여기서 직접 UsePatient 도 부르면 두 번 그리고, 더 나쁘게는
|
||||
// 세션과 창이 어긋나는 경로가 하나 더 생긴다.
|
||||
PatientSession.Set(picked, label);
|
||||
}
|
||||
|
||||
/// <summary>환자를 뗀다 — 종이에서 값이 사라지고 사유로 돌아가는 것까지 확인해야 한다</summary>
|
||||
private void OnClearPatient(object sender, RoutedEventArgs e) => UsePatient(null, string.Empty);
|
||||
private void OnClearPatient(object sender, RoutedEventArgs e)
|
||||
=> PatientSession.Set(null, string.Empty);
|
||||
|
||||
private void OnPatientSessionChanged(object? sender, EventArgs e)
|
||||
=> UsePatient(PatientSession.Current, PatientSession.Label);
|
||||
|
||||
/// <summary>
|
||||
/// 환자를 붙이거나 뗀다 — 해석기 교체·표시·다시 그리기를 한 군데에 모은다.
|
||||
@@ -206,9 +217,6 @@ public partial class PreviewWindow : Window
|
||||
internal void UsePatient(SheetMe.Data.Stores.PatientContext? picked, string label)
|
||||
{
|
||||
patient = picked;
|
||||
// 세션에도 올린다 — 쿼리 편집기의 '검증 실행'이 같은 환자로 돌아야 한다.
|
||||
// 창마다 따로 들고 있으면 같은 서식을 두 화면에서 서로 다른 환자로 보게 된다.
|
||||
PatientSession.Set(picked, label);
|
||||
tags = picked is null ? session : new PatientTagResolver(picked, session);
|
||||
// 러너를 <b>새로</b> 만든다. 같은 러너를 재사용하면 옛 환자의 조회 결과가 캐시에 남아
|
||||
// 환자를 바꿨는데 표는 그대로가 된다 — 태그만 바뀌고 표는 안 바뀌면 아무도 눈치채지 못한다.
|
||||
|
||||
@@ -139,6 +139,9 @@ public partial class QueryEditorWindow : Window
|
||||
RebuildBucketChips();
|
||||
ApplyVariableFilter(string.Empty);
|
||||
RefreshPatientButton();
|
||||
// 제목줄이나 미리보기에서 환자를 바꿔도 이 버튼이 따라와야 한다
|
||||
Services.PatientSession.Changed += OnPatientSessionChanged;
|
||||
Closed += (_, _) => Services.PatientSession.Changed -= OnPatientSessionChanged;
|
||||
var connection = Services.ConfigService.Current.ConnectionString;
|
||||
workbench = new SheetMe.Data.Stores.OracleQueryWorkbench(connection);
|
||||
if (!workbench.CanUseDb)
|
||||
@@ -204,16 +207,18 @@ public partial class QueryEditorWindow : Window
|
||||
// 이미 고른 환자가 있으면 이 버튼은 '해제'다. 바꾸려면 해제하고 다시 고른다 —
|
||||
// 버튼 하나에 '고르기/바꾸기/해제'를 다 넣으면 무엇이 일어날지 예측할 수 없다.
|
||||
Services.PatientSession.Set(null, string.Empty);
|
||||
RefreshPatientButton();
|
||||
return;
|
||||
}
|
||||
if (PatientPickerDialogView.Pick(this, out var label) is { } picked)
|
||||
{
|
||||
Services.PatientSession.Set(picked, label);
|
||||
RefreshPatientButton();
|
||||
}
|
||||
// 버튼 글자는 Changed 를 듣고 갱신한다 — 여기서 직접 부르면
|
||||
// 제목줄에서 바꿨을 때만 어긋나는 경로가 하나 생긴다.
|
||||
}
|
||||
|
||||
private void OnPatientSessionChanged(object? sender, EventArgs e) => RefreshPatientButton();
|
||||
|
||||
/// <summary>버튼이 지금 무엇을 하는지 글자로 말한다 — 상태를 색으로만 알리면 못 읽는다</summary>
|
||||
private void RefreshPatientButton()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user