diff --git a/src/SheetMe.Designer/Diagnostics/EditSmoke.cs b/src/SheetMe.Designer/Diagnostics/EditSmoke.cs
index 32d5f35..bfe3eb8 100644
--- a/src/SheetMe.Designer/Diagnostics/EditSmoke.cs
+++ b/src/SheetMe.Designer/Diagnostics/EditSmoke.cs
@@ -705,7 +705,32 @@ public static class EditSmoke
!previewCleared.Contains("C-9003", StringComparison.Ordinal)
&& previewCleared.Contains("[PAT_차트번호", StringComparison.Ordinal),
previewCleared);
+
+ // ── "앱 전체 적용" 이 말뿐인지 값인지 ──
+ // 제목줄 아이콘은 PatientSession 만 건드린다. 열려 있는 창들이 그것을 듣지 않으면
+ // 아이콘은 켜지는데 종이는 그대로다 — 사용자에게는 "설정했는데 안 먹는다"가 된다.
+ // 세션에만 쓰고 종이가 따라오는지 본다(창의 메서드를 직접 부르지 않는다).
+ 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);
+
+ // 창을 닫은 뒤에도 이벤트가 남아 있으면 회수되지 않고, 죽은 창이 계속 그린다.
+ // 닫고 다시 세션을 건드려 예외 없이 지나가는지 본다(구독이 남아 있으면
+ // 죽은 창의 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()));
diff --git a/src/SheetMe.Designer/Views/MainView.xaml b/src/SheetMe.Designer/Views/MainView.xaml
index 935e86f..fef7bb3 100644
--- a/src/SheetMe.Designer/Views/MainView.xaml
+++ b/src/SheetMe.Designer/Views/MainView.xaml
@@ -294,6 +294,14 @@
+
+
diff --git a/src/SheetMe.Designer/Views/MainView.xaml.cs b/src/SheetMe.Designer/Views/MainView.xaml.cs
index 3138ca5..668903d 100644
--- a/src/SheetMe.Designer/Views/MainView.xaml.cs
+++ b/src/SheetMe.Designer/Views/MainView.xaml.cs
@@ -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;
}
+ ///
+ /// 환자 선택 — 여기서 고르면 앱 전체가 그 환자를 본다.
+ ///
+ /// 안 골랐으면 바로 선택 창을 연다. 이미 골랐으면 메뉴를 띄운다 —
+ /// 늘 보이는 아이콘이라 "누르면 해제"로 두면 실수로 지운다.
+ /// 바꾸기·해제를 글자로 보여 주고 고르게 하는 쪽이 예측 가능하다.
+ ///
+ 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);
+ }
+ }
+ }
+
+ ///
+ /// 아이콘이 지금 상태를 말한다 — 늘 보이는 자리라 글자를 둘 폭이 없어 색으로 알린다.
+ /// 고른 환자 이름은 툴팁에 둔다(제목줄에 환자 이름을 상시 노출하지 않는다).
+ ///
+ 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 에 실제 값이 들어갑니다(앱 전체 적용)";
+ }
+
/// 플로팅 바 — 컨트롤 추가 플라이아웃 토글(닫힘 직후 재클릭 가드)
private void OnAddControlFlyoutClick(object sender, RoutedEventArgs e)
{
diff --git a/src/SheetMe.Designer/Views/PreviewWindow.xaml.cs b/src/SheetMe.Designer/Views/PreviewWindow.xaml.cs
index f49a16d..46ee557 100644
--- a/src/SheetMe.Designer/Views/PreviewWindow.xaml.cs
+++ b/src/SheetMe.Designer/Views/PreviewWindow.xaml.cs
@@ -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 가 이 창을 포함한 모든 화면을 갱신한다.
+ // 여기서 직접 UsePatient 도 부르면 두 번 그리고, 더 나쁘게는
+ // 세션과 창이 어긋나는 경로가 하나 더 생긴다.
+ PatientSession.Set(picked, label);
}
/// 환자를 뗀다 — 종이에서 값이 사라지고 사유로 돌아가는 것까지 확인해야 한다
- 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);
///
/// 환자를 붙이거나 뗀다 — 해석기 교체·표시·다시 그리기를 한 군데에 모은다.
@@ -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);
// 러너를 새로 만든다. 같은 러너를 재사용하면 옛 환자의 조회 결과가 캐시에 남아
// 환자를 바꿨는데 표는 그대로가 된다 — 태그만 바뀌고 표는 안 바뀌면 아무도 눈치채지 못한다.
diff --git a/src/SheetMe.Designer/Views/QueryEditorWindow.xaml.cs b/src/SheetMe.Designer/Views/QueryEditorWindow.xaml.cs
index f187029..e3d0fa0 100644
--- a/src/SheetMe.Designer/Views/QueryEditorWindow.xaml.cs
+++ b/src/SheetMe.Designer/Views/QueryEditorWindow.xaml.cs
@@ -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();
+
/// 버튼이 지금 무엇을 하는지 글자로 말한다 — 상태를 색으로만 알리면 못 읽는다
private void RefreshPatientButton()
{