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;
///
/// 미리보기에 쓸 실제 환자·내원을 고른다.
///
/// 왜 모달인가. 30초 안에 끝나는 자족적인 일이고(찾기 → 고르기), 권한 문이 걸린 동작이다.
/// 사이드 패널로 두면 미리보기 종이와 나란히 놓여 항상 열려 있게 되는데,
/// 이 창은 실제 환자 정보를 꺼내는 창이라 열려 있는 것 자체가 비용이다.
///
/// 왜 두 목록을 한 화면에 두는가. 고르는 대상은 환자가 아니라 내원이다.
/// 환자 문맥 5행이 전부 내원번호와 적용일시로 읽히므로()
/// 환자만 골라서는 아무것도 읽을 수 없다. 환자를 고른 뒤 화면이 바뀌면 "이 사람 맞나"를
/// 다시 확인하러 왕복해야 한다.
///
/// 문맥을 여기서 읽는다. 창을 닫고 나서 읽으면 실패를 미리보기 창에서 알려야 하고,
/// 그때는 이미 고른 것이 사라진 뒤다. 여기서 읽어 실패하면 창을 닫지 않는다.
///
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
/// 고른 내원의 환자 문맥 — 취소했으면 null
public PatientContext? Picked { get; private set; }
/// 고른 것을 사람에게 보여 줄 한 줄. 미리보기 창 머리에 그대로 쓴다
public string PickedLabel { get; private set; } = string.Empty;
///
/// 권한을 확인하고 창을 띄운다 — 못 쓰는 사용자에게는 사유를 말한다.
/// 문을 여기 한 군데만 두면 호출부가 늘어도 빠뜨릴 곳이 없다.
///
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;
}
// 감사 기록은 고른 직후 남긴다 — 종이에 값이 찍히는 것과 무관하게 조회는 이미 일어났다.
// 환자 이름은 남기지 않는다(내원번호로 추적 가능하고, 로그가 개인정보를 들고 있으면 로그가 위험물이 된다).
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);
}
}
/// 검색 유형의 사람이 읽는 이름 — 0건 안내가 "무엇으로 찾았는지" 말할 때 쓴다
private static string KindLabel(PatientSearchKind kind) => kind switch
{
PatientSearchKind.Name => "성명 접두일치",
PatientSearchKind.ResidentNumber => "주민번호 접두일치",
PatientSearchKind.MobilePhone => "휴대전화 부분일치",
_ => "차트번호 완전일치",
};
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);
var padded = string.Empty;
// 차트번호는 완전일치다. 병원마다 자릿수가 정해져 있어 실제 값은 0으로 채워져 있는데
// (P_PatInf 가 고정 폭이다) 사람은 "12345" 처럼 앞의 0 을 빼고 친다 — 그러면 0건이다.
// 저장소는 주석으로 "병원별 zero-pad 후 전달"을 요구하는데 이 호출부가 그걸 안 지켰다.
//
// 자릿수를 설정으로 정하지 않는다. 병원마다 다르고 틀리면 멀쩡한 검색을 망친다.
// 대신 0건일 때만 한 번 더 찾는다 — 8·10자리는 레거시가 쓰던 두 폭이다.
// 찾았으면 무엇으로 찾았는지 칸에 되써서 사용자가 그 번호를 기억하게 한다.
if (found.Count == 0 && kind == PatientSearchKind.ChartNumber
&& TermBox.Text.Trim() is { Length: > 0 } typed && typed.All(char.IsDigit))
{
foreach (var width in new[] { 8, 10 })
{
if (typed.Length >= width)
{
continue;
}
var retry = store.Search(kind, typed.PadLeft(width, '0'));
if (retry.Count > 0)
{
found = retry;
padded = typed.PadLeft(width, '0');
TermBox.Text = padded;
break;
}
}
}
PatientList.ItemsSource = found;
PatientCountText.Text = found.Count switch
{
// 0건일 때 무엇으로 찾았는지 말한다 — 유형이 틀려서 0건인 경우가 가장 흔한데
// 전에는 그 힌트가 화면에 하나도 없어 접속이나 권한을 의심하게 됐다
0 => $"환자 — 없습니다({KindLabel(kind)}로 찾았습니다)",
_ when padded.Length > 0 => $"환자 {found.Count}명 — 자릿수를 채워 '{padded}' 로 찾았습니다",
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();
/// 고른 것을 글로 확인시킨다 — 목록 두 개짜리 화면에서 무엇이 선택됐는지는 쉽게 놓친다
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
}