환자를 골라 태그에 값이 들어간다 — 데이터 계층만 있던 것을 화면에 붙인다

전 커밋까지 환자 조회·시각 판정·권한 규칙은 다 있었지만 화면에 붙은 것이
하나도 없었다. 미리보기를 열면 환자 선택이 없고, 태그는 전부 사유만 찍혔다.

## 만든 사슬

PatientPickerDialogView(모달) → PatientContextStore(문맥 5행)
→ PatientTagResolver(태그 → 컬럼) → PreviewWindow.UsePatient → 종이

모달로 둔 이유: 30초 안에 끝나는 자족적인 일이고 권한 문이 걸린 동작이다.
사이드 패널로 두면 실제 환자 정보를 꺼내는 창이 항상 열려 있게 되는데
이 창은 열려 있는 것 자체가 비용이다.

환자와 내원을 한 화면에 둔다. 고르는 대상은 환자가 아니라 내원이다 —
문맥 5행이 전부 ComNum + AdpDtm 으로 읽히므로 환자만 골라서는 아무것도 못 읽는다.

기준 시점(마지막/처음)을 화면에 노출한다. 입원 내원의 진료과·병실·자격 값을
실제로 바꾸는 값이라 숨기면 값이 왜 다른지 설명할 수 없다.

문맥 읽기를 창 안에서 한다. 닫고 나서 읽으면 실패를 미리보기 창에서 알려야 하고
그때는 이미 고른 것이 사라진 뒤다.

## 매핑은 확인된 6종만

PAT_이름 / PAT_차트번호 / PAT_주민번호 / PAT_휴대전화 / PAT_전화번호 / PAT_주소.
전부 레거시 조회 SQL 의 SELECT 목록에서 컬럼을 직접 확인한 것이다.

PAT_나이·PAT_성별은 넣지 않았다 — UDF_GETAGE·UDF_GETSEX 를 거치는 값이라
컬럼을 짐작해 넣으면 값이 조용히 틀린다. 종이에 그럴듯한 값이 찍히면
아무도 의심하지 않으므로 빈칸보다 나쁘다.

안 옮긴 환자 태그는 "아직 옮기지 않았습니다"라고 말한다.
"DB 접속이 없습니다"로 뭉개면 환자를 골랐는데도 그 사유가 나와 접속을 의심하게 된다.

## 문맥 조회를 한 번에 다섯 번 읽는다

레거시는 5행을 프로퍼티 접근 시점에 하나씩 지연 로딩한다(각각 별도 왕복).
태그마다 다시 읽으면 태그 40개짜리 서식에 왕복 40회가 된다.

## 개인정보

- 감사 로그는 고른 직후 남긴다 — 종이에 값이 찍히는 것과 무관하게 조회는 이미 일어났다.
- 로그에 환자 이름을 남기지 않는다(내원번호로 추적 가능하고, 로그가
  개인정보를 들고 있으면 로그 자체가 위험물이 된다).
- 진단 리포트도 값 대신 길이만 적는다.
- 권한 문은 수정(TK_MODIFY)과 같은 범위다. 넓히면 서식생성기가 환자 조회 도구가 된다.
- 화면에서 감사 기록 사실을 말한다.

## 게이트가 잡은 것 셋

① --db-patient 가 "전건 통과"인데 ⑧~⑬ 이 통째로 SKIP 이었다.
   첫 환자에게 내원이 0건이라 문맥 조회를 한 번도 안 돌린 채 초록불이었다.
   내원 있는 환자를 30명까지 훑고, 못 찾으면 SKIP 이 아니라 FAIL 로 바꿨다.

② 그렇게 찾은 내원에서 CodInf·CoiInf·CowInf 가 셋 다 0행이었다.
   0 을 적어 두는 것으로는 "조건이 어긋났다"와 "행이 없다"를 못 가른다.
   조건 없는 원본 행을 함께 찍게 하니 후자였다(1988년 내원, 행 자체가 없음).

③ 그러면 기간 조회 세 갈래는 아직 한 번도 행을 돌려준 적이 없다.
   안 돌아본 갈래는 통과한 갈래가 아니므로, 각 표에서 직접 내원을 찾아 들어가
   BETWEEN·MtiSeq 조건이 실제로 행을 뽑는 것까지 확인하게 했다(⑮).

--edit-smoke 판정 7건 추가. 전부 대조군이 있다 —
환자 없이 같은 종이를 찍어 사유가 나오는 것을 먼저 확인한 뒤 값으로 바뀌는 것을 본다.
미리보기 창 배선도 따로 본다(붙이면 값, 떼면 사라짐). 그래서 고르는 일과
붙이는 일을 나눠 두었다 — 모달에 묻어 두면 "골랐는데 종이가 그대로"를 자동으로 못 잡는다.

## 게이트

- dotnet test 312/312
- --edit-smoke 실패 0 (환자·미리보기 판정 7건 추가)
- --dialog-shots FAIL 0 (08b-patient-picker 추가)
- --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 (변화 없음)
- --db-smoke 1,271건 diff 0
- --db-patient ①~⑮ 전건 통과 (기간 조회 3갈래 실제 통과 확인)
- --db-modify-smoke S999 8/8 (⑤ 는 E_SctMst 0행으로 여전히 SKIP)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Msystech
2026-08-18 15:13:48 +09:00
co-authored by Claude Opus 5
parent be9889ed4e
commit 88dab2edcf
11 changed files with 1008 additions and 6 deletions
@@ -0,0 +1,146 @@
<Window x:Class="SheetMe.Designer.Views.PatientPickerDialogView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="환자 선택 — 미리보기" Width="1040" Height="620"
MinWidth="880" MinHeight="480"
WindowStartupLocation="CenterOwner" ShowInTaskbar="False"
Style="{StaticResource ThemedWindow}">
<DockPanel Margin="12">
<!-- 검색 줄. 유형을 먼저 고른다 — 유형마다 일치 방식이 달라서(차트번호는 완전일치,
성명은 접두, 휴대전화는 부분) 한 칸에 몰아넣으면 왜 안 찾아지는지 설명할 수 없다. -->
<Grid DockPanel.Dock="Top" Margin="0,0,0,10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="검색" VerticalAlignment="Center" Margin="0,0,8,0"
Foreground="{DynamicResource B.Muted}"/>
<ComboBox x:Name="KindCombo" Grid.Column="1" Width="110" SelectedIndex="0"
AutomationProperties.Name="검색 유형">
<ComboBoxItem Content="차트번호"/>
<ComboBoxItem Content="성명"/>
<ComboBoxItem Content="주민번호"/>
<ComboBoxItem Content="휴대전화"/>
</ComboBox>
<TextBox x:Name="TermBox" Grid.Column="2" Margin="8,0" VerticalContentAlignment="Center"
KeyDown="OnTermKeyDown" AutomationProperties.Name="검색어"/>
<Button x:Name="SearchButton" Grid.Column="3" Content="찾기(_F)" Padding="18,4" Click="OnSearch"/>
</Grid>
<!-- 감사 고지. 이 창은 실제 환자 정보를 꺼낸다 — 기록된다는 사실을 화면에서 말한다. -->
<TextBlock DockPanel.Dock="Top" Margin="0,0,0,8" TextWrapping="Wrap"
Foreground="{DynamicResource B.Muted}"
Text="실제 환자 정보를 불러옵니다. 조회한 사용자와 내원번호가 감사 로그에 남습니다(환자 이름은 남기지 않습니다)."/>
<!-- 아래 줄. 고른 것을 글로 확인시키고 기준 시점을 함께 둔다.
기준 시점은 입원 내원의 진료과·병실·자격 값을 실제로 바꾼다 — 숨기면 값이 왜 다른지 알 수 없다. -->
<Grid DockPanel.Dock="Bottom" Margin="0,10,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock x:Name="ChosenText" Grid.Column="0" VerticalAlignment="Center"
TextTrimming="CharacterEllipsis" Foreground="{DynamicResource B.Muted}"
Text="환자를 찾아 내원을 고르세요."/>
<StackPanel Grid.Column="1" Orientation="Horizontal">
<TextBlock Text="기준 시점" VerticalAlignment="Center" Margin="0,0,6,0"
Foreground="{DynamicResource B.Muted}"
ToolTip="입원 내원은 구간이라 어느 시점의 진료과·병실·보험 자격을 쓸지 정해야 합니다. 외래는 접수일시로 같습니다."/>
<ComboBox x:Name="EdgeCombo" Width="92" SelectedIndex="0"
AutomationProperties.Name="기준 시점">
<ComboBoxItem Content="마지막"/>
<ComboBoxItem Content="처음"/>
</ComboBox>
<Button x:Name="SelectButton" Content="선택" Padding="20,4" Margin="10,0,0,0"
Click="OnSelect" IsDefault="True" IsEnabled="False"/>
<Button Content="취소" Padding="18,4" Margin="8,0,0,0" IsCancel="True"/>
</StackPanel>
</Grid>
<!-- 본문. 환자 목록과 그 환자의 내원 목록을 동시에 둔다 —
고르는 대상은 내원이므로 환자를 고른 뒤 화면이 바뀌면 왕복이 생긴다. -->
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="47*"/>
<ColumnDefinition Width="6"/>
<ColumnDefinition Width="53*"/>
</Grid.ColumnDefinitions>
<DockPanel Grid.Column="0">
<TextBlock x:Name="PatientCountText" DockPanel.Dock="Top" Margin="0,0,0,4"
Foreground="{DynamicResource B.Muted}"
Text="환자 — 검색어를 넣고 찾기를 누르세요"/>
<ListView x:Name="PatientList" SelectionChanged="OnPatientChanged"
BorderBrush="{DynamicResource B.Line}" BorderThickness="1"
VirtualizingPanel.IsVirtualizing="True"
AutomationProperties.Name="환자 목록">
<ListView.View>
<GridView>
<GridViewColumn Header="차트번호" Width="90" DisplayMemberBinding="{Binding ChtNum}"/>
<GridViewColumn Header="성명" Width="80" DisplayMemberBinding="{Binding Name}"/>
<GridViewColumn Header="주민번호" Width="105" DisplayMemberBinding="{Binding ResNumMasked}"/>
<GridViewColumn Header="휴대전화" Width="100" DisplayMemberBinding="{Binding MobilePhone}"/>
<GridViewColumn Header="비고" Width="70">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock>
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Foreground" Value="{DynamicResource B.Muted}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Inpatient}" Value="True">
<Setter Property="Text" Value="재원"/>
<Setter Property="Foreground" Value="{DynamicResource B.Accent}"/>
</DataTrigger>
<DataTrigger Binding="{Binding Active}" Value="False">
<Setter Property="Text" Value="사용중지"/>
</DataTrigger>
<DataTrigger Binding="{Binding Shielded}" Value="True">
<Setter Property="Text" Value="정보보호"/>
<Setter Property="Foreground" Value="{DynamicResource B.Danger}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
</DockPanel>
<GridSplitter Grid.Column="1" Width="6" HorizontalAlignment="Stretch"
Background="Transparent"/>
<DockPanel Grid.Column="2">
<TextBlock x:Name="VisitCountText" DockPanel.Dock="Top" Margin="0,0,0,4"
Foreground="{DynamicResource B.Muted}"
Text="내원 — 환자를 고르면 나옵니다"/>
<ListView x:Name="VisitList" MouseDoubleClick="OnSelect"
SelectionChanged="OnVisitChanged"
BorderBrush="{DynamicResource B.Line}" BorderThickness="1"
VirtualizingPanel.IsVirtualizing="True"
AutomationProperties.Name="내원 목록">
<ListView.View>
<GridView>
<GridViewColumn Header="구분" Width="52" DisplayMemberBinding="{Binding Kind}"/>
<GridViewColumn Header="접수일시" Width="118" DisplayMemberBinding="{Binding AcceptedAtText}"/>
<GridViewColumn Header="진료과" Width="100" DisplayMemberBinding="{Binding Department}"/>
<GridViewColumn Header="담당의" Width="80" DisplayMemberBinding="{Binding Doctor}"/>
<GridViewColumn Header="보험" Width="86" DisplayMemberBinding="{Binding Insurance}"/>
<GridViewColumn Header="상태" Width="66" DisplayMemberBinding="{Binding State}"/>
</GridView>
</ListView.View>
</ListView>
</DockPanel>
</Grid>
</DockPanel>
</Window>
@@ -0,0 +1,232 @@
using System.Windows;
using System.Windows.Input;
using SheetMe.Core.Catalog;
using SheetMe.Data.Stores;
using SheetMe.Designer.DataBusiness;
using SheetMe.Designer.Services;
namespace SheetMe.Designer.Views;
/// <summary>
/// 미리보기에 쓸 <b>실제 환자·내원</b>을 고른다.
///
/// <b>왜 모달인가.</b> 30초 안에 끝나는 자족적인 일이고(찾기 → 고르기), 권한 문이 걸린 동작이다.
/// 사이드 패널로 두면 미리보기 종이와 나란히 놓여 항상 열려 있게 되는데,
/// 이 창은 실제 환자 정보를 꺼내는 창이라 <b>열려 있는 것 자체가 비용</b>이다.
///
/// <b>왜 두 목록을 한 화면에 두는가.</b> 고르는 대상은 환자가 아니라 <b>내원</b>이다.
/// 환자 문맥 5행이 전부 내원번호와 적용일시로 읽히므로(<see cref="PatientContextStore"/>)
/// 환자만 골라서는 아무것도 읽을 수 없다. 환자를 고른 뒤 화면이 바뀌면 "이 사람 맞나"를
/// 다시 확인하러 왕복해야 한다.
///
/// <b>문맥을 여기서 읽는다.</b> 창을 닫고 나서 읽으면 실패를 미리보기 창에서 알려야 하고,
/// 그때는 이미 고른 것이 사라진 뒤다. 여기서 읽어 실패하면 창을 닫지 않는다.
/// </summary>
public partial class PatientPickerDialogView : Window
{
#region Member Fields
private readonly PatientVisitStore? store;
private readonly PatientContextStore? contexts;
#endregion
#region Constructors
public PatientPickerDialogView()
{
InitializeComponent();
var connection = ConfigService.Current.ConnectionString;
if (connection.Length > 0)
{
store = new PatientVisitStore(connection);
contexts = new PatientContextStore(connection);
}
Loaded += (_, _) => TermBox.Focus();
}
#endregion
#region Methods
/// <summary>고른 내원의 환자 문맥 — 취소했으면 null</summary>
public PatientContext? Picked { get; private set; }
/// <summary>고른 것을 사람에게 보여 줄 한 줄. 미리보기 창 머리에 그대로 쓴다</summary>
public string PickedLabel { get; private set; } = string.Empty;
/// <summary>
/// 권한을 확인하고 창을 띄운다 — 못 쓰는 사용자에게는 사유를 말한다.
/// 문을 여기 한 군데만 두면 호출부가 늘어도 빠뜨릴 곳이 없다.
/// </summary>
public static PatientContext? Pick(Window? owner, out string label)
{
label = string.Empty;
if (!FormDesignDataBusiness.CanPreviewPatient())
{
DialogService.Notify(DialogKind.Warning, "환자 미리보기",
"실제 환자로 미리보기를 할 수 없습니다.",
FormDesignDataBusiness.PreviewPatientDenyReason(), owner);
return null;
}
var dialog = new PatientPickerDialogView { Owner = owner };
if (dialog.ShowDialog() != true || dialog.Picked is not { } picked)
{
return null;
}
// 감사 기록은 <b>고른 직후</b> 남긴다 — 종이에 값이 찍히는 것과 무관하게 조회는 이미 일어났다.
// 환자 이름은 남기지 않는다(내원번호로 추적 가능하고, 로그가 개인정보를 들고 있으면 로그가 위험물이 된다).
AppLog.Audit(PatientPreviewPolicy.AuditLine(
UserSession.Current.UidCod, picked.ComNum.ToString("F0")));
label = dialog.PickedLabel;
return picked;
}
private void OnTermKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
e.Handled = true;
OnSearch(sender, e);
}
}
private void OnSearch(object sender, RoutedEventArgs e)
{
if (store is null)
{
DialogService.Notify(DialogKind.Warning, "환자 검색",
"DB 에 접속되어 있지 않습니다.", string.Empty, this);
return;
}
var kind = KindCombo.SelectedIndex switch
{
1 => PatientSearchKind.Name,
2 => PatientSearchKind.ResidentNumber,
3 => PatientSearchKind.MobilePhone,
_ => PatientSearchKind.ChartNumber,
};
VisitList.ItemsSource = null;
VisitCountText.Text = "내원";
try
{
// 동기로 둔다. 행 상한이 200이라 오래 걸리지 않고, 비동기로 만들면
// "검색 중에 또 검색"을 다뤄야 해서 이 창 크기에 맞지 않는 복잡도가 생긴다.
Mouse.OverrideCursor = Cursors.Wait;
SearchButton.IsEnabled = false;
var found = store.Search(kind, TermBox.Text);
PatientList.ItemsSource = found;
PatientCountText.Text = found.Count switch
{
0 => "환자 — 없습니다",
PatientVisitStore.DefaultLimit =>
$"환자 {found.Count}명 — 상한에 걸렸습니다. 검색어를 좁히세요",
_ => $"환자 {found.Count}명",
};
if (found.Count > 0)
{
PatientList.SelectedIndex = 0;
PatientList.Focus();
}
}
catch (Exception ex)
{
DialogService.Notify(DialogKind.Error, "환자 검색", "검색 중 오류가 발생했습니다.", ex.Message, this);
}
finally
{
SearchButton.IsEnabled = true;
Mouse.OverrideCursor = null;
}
}
private void OnPatientChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
{
UpdateChosen();
if (store is null || PatientList.SelectedItem is not PatientSummary patient)
{
return;
}
try
{
Mouse.OverrideCursor = Cursors.Wait;
var visits = store.ListVisits(patient.ChtNum);
VisitList.ItemsSource = visits;
VisitCountText.Text = visits.Count == 0 ? "내원 — 없습니다" : $"내원 {visits.Count}건";
if (visits.Count > 0)
{
VisitList.SelectedIndex = 0;
}
}
catch (Exception ex)
{
DialogService.Notify(DialogKind.Error, "내원 조회", "내원을 읽지 못했습니다.", ex.Message, this);
}
finally
{
Mouse.OverrideCursor = null;
}
}
private void OnVisitChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
=> UpdateChosen();
/// <summary>고른 것을 글로 확인시킨다 — 목록 두 개짜리 화면에서 무엇이 선택됐는지는 쉽게 놓친다</summary>
private void UpdateChosen()
{
var patient = PatientList.SelectedItem as PatientSummary;
var visit = VisitList.SelectedItem as VisitSummary;
SelectButton.IsEnabled = patient is not null && visit is not null;
ChosenText.Text = (patient, visit) switch
{
(null, _) => "환자를 찾아 내원을 고르세요.",
({ } p, null) => $"{p.Name}({p.ChtNum}) — 내원을 고르세요.",
({ } p, { } v) => $"{p.Name}({p.ChtNum}) · {v.Kind} {v.AcceptedAtText} · 내원 {v.ComNum:F0}",
};
}
private void OnSelect(object sender, RoutedEventArgs e)
{
if (PatientList.SelectedItem is not PatientSummary patient
|| VisitList.SelectedItem is not VisitSummary visit)
{
return;
}
if (contexts is null)
{
DialogService.Notify(DialogKind.Warning, "환자 미리보기",
"DB 에 접속되어 있지 않습니다.", string.Empty, this);
return;
}
var edge = EdgeCombo.SelectedIndex == 1 ? VisitEdge.First : VisitEdge.Last;
try
{
Mouse.OverrideCursor = Cursors.Wait;
var context = contexts.Load(visit.ComNum, edge);
if (context is null)
{
// 목록에는 있는데 다시 읽을 때 없다 — 그 사이에 지워졌거나 권한이 다르다.
// 창을 닫지 않는다. 닫으면 미리보기가 조용히 빈 종이를 낸다.
DialogService.Notify(DialogKind.Warning, "환자 미리보기",
"이 내원의 정보를 읽지 못했습니다.",
$"내원 {visit.ComNum:F0} 을 다시 읽을 수 없습니다. 목록을 새로 검색해 보세요.", this);
return;
}
Picked = context;
PickedLabel = $"{patient.Name}({patient.ChtNum}) · {visit.Kind}"
+ $" {visit.AcceptedAtText} · 기준 {Readable(context.AdpDtm)}";
DialogResult = true;
}
catch (Exception ex)
{
DialogService.Notify(DialogKind.Error, "환자 미리보기",
"환자 정보를 읽는 중 오류가 발생했습니다.", ex.Message, this);
}
finally
{
Mouse.OverrideCursor = null;
}
}
private static string Readable(string stamp)
=> stamp.Length == 12
? $"{stamp[..4]}-{stamp[4..6]}-{stamp[6..8]} {stamp[8..10]}:{stamp[10..12]}"
: stamp;
#endregion
}
@@ -21,6 +21,19 @@
Checked="OnViewToggled" Unchecked="OnViewToggled"
ToolTip="끄면 화면에 보이는 대로 — 인쇄 출력을 끈 컨트롤도 보입니다(레거시 미리보기와 같음)"/>
<Separator/>
<!--
실제 환자로 미리보기 — 태그가 값을 뽑는지 확인하는 유일한 방법이다.
고르지 않은 상태가 기본이다. 여는 순간 환자를 붙이면 미리보기를 열 때마다
환자 정보를 조회하게 되고, 그건 이 도구가 할 일이 아니다.
-->
<Button x:Name="PatientButton" Content="환자 선택..." Padding="10,3" Click="OnPickPatient"
ToolTip="실제 환자·내원을 골라 태그에 값이 들어간 상태로 확인합니다"/>
<Button x:Name="PatientClearButton" Content="환자 해제" Padding="10,3" Click="OnClearPatient"
IsEnabled="False" ToolTip="환자 없이 — 태그는 사유만 보입니다"/>
<TextBlock x:Name="PatientText" VerticalAlignment="Center" Margin="8,0"
MaxWidth="360" TextTrimming="CharacterEllipsis"
Foreground="{DynamicResource B.Muted}" Text="환자 없음"/>
<Separator/>
<Button Content="" Padding="8,3" Click="OnZoomOut"/>
<TextBlock x:Name="ZoomText" Text="100%" VerticalAlignment="Center" Margin="6,0" MinWidth="42" TextAlignment="Center"/>
<Button Content="" Padding="8,3" Click="OnZoomIn"/>
@@ -28,10 +28,17 @@ public partial class PreviewWindow : Window
private double zoom = 1.0;
/// <summary>
/// 태그 해석기 — 창당 하나. DB 재료를 한 번만 읽고 창이 닫힐 때까지 들고 있다.
/// 시각·로그인 사용자 태그 해석기 — 창당 하나. DB 재료를 한 번만 읽고 창이 닫힐 때까지 들고 있다.
/// 그리기마다 새로 만들면 다시 그릴 때마다 DB 를 친다.
/// </summary>
private readonly SessionTagResolver tags = new();
private readonly SessionTagResolver session = new();
/// <summary>
/// 지금 쓰는 해석기. 환자를 고르면 <see cref="PatientTagResolver"/> 가 앞에 붙고,
/// 해제하면 다시 <see cref="session"/> 뿐이다 — 환자를 해제했는데 옛 값이 남으면
/// 화면이 실제와 다른 것을 보여 준다.
/// </summary>
private SheetMe.Core.Catalog.ITagValueResolver tags;
#endregion
#region Constructors
@@ -42,6 +49,7 @@ public partial class PreviewWindow : Window
internal PreviewWindow(DesignerViewModel designer)
{
this.designer = designer;
tags = session;
InitializeComponent();
Activated += (_, _) => BuildPages();
Closed += (_, _) =>
@@ -156,6 +164,40 @@ public partial class PreviewWindow : Window
private void OnRefresh(object sender, RoutedEventArgs e) => BuildPages();
/// <summary>
/// 실제 환자를 골라 태그에 값을 넣는다.
///
/// 권한 문·감사 기록·문맥 읽기는 모두 <see cref="PatientPickerDialogView.Pick"/> 안에 있다 —
/// 여기서 다시 확인하면 두 곳이 어긋날 수 있다.
/// </summary>
private void OnPickPatient(object sender, RoutedEventArgs e)
{
var picked = PatientPickerDialogView.Pick(this, out var label);
if (picked is null)
{
return;
}
UsePatient(picked, label);
}
/// <summary>환자를 뗀다 — 종이에서 값이 사라지고 사유로 돌아가는 것까지 확인해야 한다</summary>
private void OnClearPatient(object sender, RoutedEventArgs e) => UsePatient(null, string.Empty);
/// <summary>
/// 환자를 붙이거나 뗀다 — 해석기 교체·표시·다시 그리기를 한 군데에 모은다.
///
/// <b>모달과 분리하는 이유</b>: 고르는 일(권한·감사·문맥 읽기)과 붙이는 일(해석기 교체 → 종이)은
/// 다른 일이고, 붙이는 쪽만 진단이 확인할 수 있어야 한다. 창을 띄우는 코드에 묻어 두면
/// "환자를 골랐는데 종이가 그대로다"를 자동으로 잡을 방법이 없다 — 그게 이 단계에서 가장 무서운 고장이다.
/// </summary>
internal void UsePatient(SheetMe.Data.Stores.PatientContext? picked, string label)
{
tags = picked is null ? session : new PatientTagResolver(picked, session);
PatientText.Text = picked is null ? "환자 없음" : label;
PatientClearButton.IsEnabled = picked is not null;
BuildPages();
}
/// <summary>보기 전환 — 인쇄 기준 / 화면 그대로</summary>
private void OnViewToggled(object sender, RoutedEventArgs e) => BuildPages();