Files
SheetMe/src/SheetMe.Designer/Views/RecordWordDialogView.xaml.cs
T
MsystechandClaude Opus 5 604f8f7d1b 순정 MessageBox 를 앱 테마 대화상자로 교체 (53곳 중 49곳)
다크 테마에서 흰 시스템 창이 튀어나오던 알림을 자체 창(MessageDialogView)으로 바꿨다.
앱 셸과 같은 커스텀 타이틀바(36px) + B.Surface 본문 + 성격 아이콘 + 우측 버튼 줄.
폭 440 고정, 내용에 따라 높이 자동, 긴 예외는 스크롤로 흡수한다.

진입점은 DialogService 정적 메서드 4개로 모았다 —
Notify / Confirm / ConfirmWithCancel / ShowError.

MessageBox 는 Win32 호출이라 아무 데서나 되지만 WPF 창은 아니다. 교체로 새로 생기는
제약 셋을 DialogService 한곳에서 막는다.
- 진단 모드에서는 창을 만들지 않고 기각 기본값을 즉시 돌려준다. 스모크는 무인 실행이라
  모달이 하나라도 뜨면 타임아웃 없이 영원히 멈춘다 — 실제로 편집 스모크가 지나는 경로에
  붙여넣기·개명 가드가 있다. 규약을 스모크 검사 3건으로 고정했다.
- UI 스레드가 아니면 Dispatcher 로 넘긴다.
- 소유 창은 '살아 있는 것'만 건다. 아직 안 보였거나 이미 닫힌 창을 Owner 로 주면 예외이고,
  진단 렌더러가 도는 동안 MainWindow 가 닫힌 창을 가리킬 수 있다.

App.xaml.cs 4곳은 다르게 처리했다.
- 기동 실패(:49)·예외 폭주(:82)·복구 안내(:89) 3곳은 순정 유지. 창이 없는 시점이라
  자체 창을 띄우면 종료코드가 유실되고, 이미 예외가 터진 자리에서 WPF 창을 새로 만들면
  같은 핸들러로 재진입한다. 이유를 각 자리에 주석으로 남겼다.
  덤으로 :89 는 e.Handled 를 알림보다 먼저 세우도록 순서를 바로잡았다 —
  알림이 던지면 '복구 가능한 예외'가 하드 크래시로 바뀐다.
- 알 수 없는 진단 옵션(:179)은 알림을 없앴다. 옵션 오타 하나로 무인 실행이 멈추던 자리다.

함께 고친 것 — Primary 버튼 스타일. 공유 버튼 템플릿의 호버 트리거가 TargetName 으로
채움을 회색으로 덮는데(TargetName 트리거는 TemplateBinding 을 이긴다) 글자는 흰색 그대로라,
라이트에서 마우스를 올리면 #F3F3F3 위 흰 글자 1.08:1 로 사라진다. 지금까지 Primary 사용처가
0건이라 드러난 적이 없었고 이 창이 첫 사용이다. 전용 템플릿 + 채움 호버·누름 토큰 2종 신설.

신설 토큰: B.Warning / B.Danger(라이트는 다크값을 못 쓴다 — 앰버 #E0A33A 는 흰 면 위 2.22:1 로
아이콘 기준 3:1 도 미달), B.AccentFillHover / B.AccentFillPressed. 전부 양 테마에 동시 추가.
Lucide 아이콘 4종(info·circle-alert·triangle-alert·circle-help) 추가.

바꾸지 않은 것: 예외 원문. 18곳을 ShowError 로 수렴시키면 화면에서 ex.Message 가
오류코드+로그로 대체되는 동작 변경이 된다 — 요청은 시각 변경이라 문구·정보량을 그대로 뒀다.
남는 시스템 대화상자: 인쇄(PrintDialog)와 파일 열기/저장 — OS 셸 대화상자라 대상이 아니다.

진단 렌더러에 알림 5종(오류·경고·확인·저장확인·정보)을 등록해 라이트/다크 10장이 자동으로 남는다.
회귀: 테스트 124/124, 편집 스모크 실패 0, DB 왕복 1,271건 diff 0/예외 0,
종이 렌더 P062 바이트 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 10:07:25 +09:00

174 lines
5.8 KiB
C#

using System.Collections.ObjectModel;
using System.Windows;
using SheetMe.Data.Stores;
using SheetMe.Designer.Services;
namespace SheetMe.Designer.Views;
/// <summary>
/// 서식별 상용구 관리 대화상자 — E_SHTWRDMST 1단계 문구 추가/수정/삭제/순서.
/// 레거시 런타임 상용구 팝업(MRecordWord)이 그대로 읽어가는 데이터를 관리한다.
/// </summary>
public partial class RecordWordDialogView : Window
{
#region Member Fields
private readonly RecordWordStore store;
private readonly string shtCod;
private readonly string auditUid;
private readonly bool canWrite;
private readonly ObservableCollection<RecordWordInfo> words = new();
#endregion
#region Constructors
/// <param name="auditUid">감사 컬럼(ShtUpdUid/ShtFstUid)에 기록할 HIS 사용자 코드</param>
/// <param name="canWrite">DB 쓰기 가능 여부 — 미인증/게이트 차단이면 읽기 전용으로 연다</param>
public RecordWordDialogView(RecordWordStore store, string shtCod, string sheetTitle, string auditUid, bool canWrite)
{
this.store = store;
this.shtCod = shtCod;
this.auditUid = auditUid;
this.canWrite = canWrite;
InitializeComponent();
HeaderText.Text = $"[{shtCod}] {sheetTitle} — 작성 화면의 상용구 팝업에 표시되는 문구입니다. " +
"(텍스트박스의 '상용구 사용'이 켜져 있어야 합니다)" +
(canWrite ? string.Empty : "\n※ HIS 사용자가 확인되지 않아 읽기 전용입니다.");
WordList.ItemsSource = words;
Loaded += (_, _) => Reload();
NewWordBox.Loaded += (_, _) => NewWordBox.Focus();
}
#endregion
#region Methods - 가드
/// <summary>쓰기 전 확인 — 미인증이면 안내 후 차단(감사 컬럼에 남길 주체가 없다)</summary>
private bool EnsureWritable()
{
if (canWrite)
{
return true;
}
DialogService.Notify(DialogKind.Warning, "상용구 관리",
"HIS 사용자가 확인되지 않아 상용구를 변경할 수 없습니다.",
"기록지정보 화면에서 서식생성기를 실행해 주세요.", this);
return false;
}
#endregion
#region Methods
private void Reload()
{
try
{
words.Clear();
foreach (var word in store.List(shtCod))
{
words.Add(word);
}
}
catch (Exception ex)
{
DialogService.Notify(DialogKind.Error, "오류", "상용구 조회 중 오류가 발생했습니다.", ex.Message, this);
}
}
private void OnAdd(object sender, RoutedEventArgs e)
{
var value = NewWordBox.Text.Trim();
if (value.Length == 0 || !EnsureWritable())
{
return;
}
try
{
store.Add(shtCod, value, auditUid);
NewWordBox.Clear();
NewWordBox.Focus();
Reload();
WordList.SelectedIndex = words.Count - 1;
}
catch (Exception ex)
{
DialogService.Notify(DialogKind.Error, "오류", "추가 중 오류가 발생했습니다.", ex.Message, this);
}
}
private void OnEdit(object sender, RoutedEventArgs e)
{
if (WordList.SelectedItem is not RecordWordInfo selected || !EnsureWritable())
{
return;
}
// QueryEditorWindow 재사용(큰 다중행 편집 영역) — SQL 이 아니므로 치환 변수 패널은 숨긴다
var editor = new QueryEditorWindow("상용구 수정", selected.Value, showVariables: false) { Owner = this };
if (editor.ShowDialog() != true)
{
return;
}
var newValue = editor.QueryText.Trim();
if (newValue.Length == 0 || newValue == selected.Value)
{
return;
}
try
{
store.Update(selected.Key, newValue, auditUid);
Reload();
}
catch (Exception ex)
{
DialogService.Notify(DialogKind.Error, "오류", "수정 중 오류가 발생했습니다.", ex.Message, this);
}
}
private void OnRemove(object sender, RoutedEventArgs e)
{
if (WordList.SelectedItem is not RecordWordInfo selected || !EnsureWritable())
{
return;
}
if (!DialogService.Confirm("상용구 삭제", "삭제할까요?", Truncate(selected.Value),
yes: "삭제", no: "취소", destructive: true, owner: this))
{
return;
}
try
{
store.Remove(selected.Key, auditUid);
Reload();
}
catch (Exception ex)
{
DialogService.Notify(DialogKind.Error, "오류", "삭제 중 오류가 발생했습니다.", ex.Message, this);
}
}
private void OnMoveUp(object sender, RoutedEventArgs e) => Move(-1);
private void OnMoveDown(object sender, RoutedEventArgs e) => Move(+1);
private void Move(int delta)
{
var index = WordList.SelectedIndex;
var target = index + delta;
if (index < 0 || target < 0 || target >= words.Count || !EnsureWritable())
{
return;
}
try
{
words.Move(index, target);
store.Reorder(words.Select(w => w.Key).ToList(), auditUid);
WordList.SelectedIndex = target;
}
catch (Exception ex)
{
DialogService.Notify(DialogKind.Error, "오류", "순서 변경 중 오류가 발생했습니다.", ex.Message, this);
Reload();
}
}
private static string Truncate(string value)
=> value.Length > 80 ? value[..80] + "…" : value;
#endregion
}