■ 중복 열기로 편집분이 통째로 사라지던 문제
중복 차단이 d.IsFromDb 를 요구했다. 그래서 XML 파일로 이미 열어 둔 서식을 목록에서
더블클릭해 DB 로 또 열 수 있었고, 두 탭에서 교대로 저장하면 낙관적 충돌 검출이
양쪽 어디에도 없어 마지막 저장이 앞선 편집을 통째로 덮었다.
레거시 MultiSheetFormAllow 는 탭 제목의 [서식코드]만 보고 출처와 무관하게 막는다
(frmSheetDesigner.vb:242-252). 같은 규칙으로 맞췄다:
· 판정을 FindOpenSheet(서식코드) 하나로 모으고 IsFromDb 조건을 뺐다.
· 열기 경로 3곳(DB / XML 파일 / JSON 가져오기)이 전부 이 지점을 지난다.
종전에는 DB 경로에만 검사가 있었다.
· 이력 탭은 계속 제외한다 — 현재본을 열려는 요청이 과거 스냅샷을 활성화하면 안 된다.
· 빈 서식코드는 서로 다른 문서로 본다. 묶으면 '새 서식'을 두 번 만들 수 없다.
· 막을 때 어느 탭으로 갔는지 상태줄에 적는다. 아무 일도 안 일어난 것처럼 보이면 사용자는 다시 누른다.
비교용으로 나란히 열어야 하는 경우를 위해 탈출구를 남겼다 —
DataConfig.AllowMultiSheetForm(기본 false, 레거시 AllowMultiSheetForm 기본 "N" 과 동일).
■ 함께 고친 것 — 이력본을 저장해도 탭이 '이력'으로 남던 문제
DB 저장 후 HistorySdgKey 를 null 로 되돌리지 않아 탭 제목이 '(이력 N)' 으로 남고,
종료 확인이 '저장할 수 없는 탭' 분기로 들어가 저장 선택지 없이
'변경을 버리고 종료할까요?' 만 물었다 — 저장 뒤의 편집분이 조용히 버려진다.
저장한 순간 그 탭은 스냅샷이 아니라 활성 디자인이므로 표식을 지운다.
이 수정이 만드는 경계가 하나 있다: 이력 탭을 되살려 저장하면 같은 서식의 다른 탭이
이제 옛 내용을 들고 있게 된다. 그 탭에서 저장하면 방금 저장분을 덮으므로,
어느 탭이 낡았는지 알리는 경고를 띄운다(자동으로 닫지는 않는다 — 미저장 편집분이 있을 수 있다).
■ 3단계 판단용 실측(--db-phase3)
사이트 확인을 기다리는 동안 DB 로 알 수 있는 것부터 쟀다:
· ShtCneYon='Y' 40건, 그중 현역 19건 — TK_MODIFY(제자리 정정) 부재의 실제 영향 범위.
· ShtDepCls 는 418건(24%)에 실제 분류가 들어 있다(산업재해·물리치료실·병동출력물…).
비어 있지 않으므로 진료과 2단계 그룹 미이식은 목록 구성을 실제로 바꾼다.
· ShtDspSeq 는 1,714건(98.7%)이 1 이상이다. 표시 순번은 사실상 전 서식에 채워져 있어,
정렬 미이식은 거의 모든 서식의 목록 순서를 레거시와 다르게 만든다.
· ShtTyp='S'(스캔형) 108건, 현역 105건. 사이트 조건 이식이 걸리는 모집단이 실재한다.
· EMRShtSortTyp 는 확인하지 못했다 — 코드 마스터(DtlCod/DtlCodVal) 테이블이
이 접속 스키마에 없다. 운영 접속에서 재측정이 필요하다.
편집 스모크 5건 추가(출처 무관 판정 / 대소문자 / 이력 제외 / 무관 서식 / 빈 코드).
회귀: 테스트 142/142, 편집 스모크 192건 실패 0,
DB 왕복 1,271건 diff 0/예외 0, 종이 렌더 P062 바이트 동일.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1007 lines
40 KiB
C#
1007 lines
40 KiB
C#
using System.Collections.ObjectModel;
|
||
using System.IO;
|
||
using System.Windows;
|
||
using M.Framework.WPF;
|
||
using SheetMe.Core.Catalog;
|
||
using SheetMe.Data.Stores;
|
||
using SheetMe.Designer.DataBusiness;
|
||
using SheetMe.Designer.Services;
|
||
|
||
namespace SheetMe.Designer.ViewModels;
|
||
|
||
/// <summary>
|
||
/// 메인 셸 ViewModel — 멀티 문서 탭(여러 기록지 동시 편집), 상시 서식 목록 패널,
|
||
/// 파일/DB 열기·저장, 도구(상용구/폰트). 컨트롤 클립보드는 문서 간 공유(서식 간 복사/붙여넣기).
|
||
/// </summary>
|
||
internal sealed class MainViewModel : ViewModelBase
|
||
{
|
||
#region Member Fields
|
||
private readonly FormDesignDataBusiness dataBusiness = new();
|
||
private readonly DialogService dialogService = new();
|
||
private DesignerViewModel? currentDesigner;
|
||
private string title = "SheetMe 서식생성기";
|
||
private string statusText = "준비";
|
||
private string sheetSearchKeyword = string.Empty;
|
||
private bool isSheetListLoading;
|
||
private int sheetGroupCount;
|
||
private bool sheetListTruncated;
|
||
private string appliedSheetKeyword = string.Empty;
|
||
private const string SheetGroupsExpandedPref = "sheetGroupsExpanded";
|
||
private bool areSheetGroupsExpanded = UserPrefs.GetBool(SheetGroupsExpandedPref, false);
|
||
private int selectedLeftTabIndex;
|
||
private bool isHandTool;
|
||
private const string XmlFilter = "서식 XML (*.xml)|*.xml|모든 파일 (*.*)|*.*";
|
||
#endregion
|
||
|
||
#region Properties
|
||
/// <summary>창 제목</summary>
|
||
public string Title
|
||
{
|
||
get => title;
|
||
set => SetProperty(ref title, value);
|
||
}
|
||
|
||
/// <summary>상태바 텍스트</summary>
|
||
public string StatusText
|
||
{
|
||
get => statusText;
|
||
set => SetProperty(ref statusText, value);
|
||
}
|
||
|
||
/// <summary>열린 문서(탭) 목록</summary>
|
||
public ObservableCollection<DesignerViewModel> OpenDesigners { get; } = new();
|
||
|
||
/// <summary>활성 문서(선택 탭)</summary>
|
||
public DesignerViewModel? CurrentDesigner
|
||
{
|
||
get => currentDesigner;
|
||
set
|
||
{
|
||
if (SetProperty(ref currentDesigner, value))
|
||
{
|
||
Title = value is null
|
||
? "SheetMe 서식생성기"
|
||
: $"SheetMe 서식생성기 — {value.DisplayName}";
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>팔레트 컨트롤 목록(레지스트리)</summary>
|
||
public IReadOnlyList<ControlDescriptor> PaletteItems => ControlRegistry.All;
|
||
|
||
/// <summary>서식 목록(상시 패널) — DB E_ShtMst</summary>
|
||
public ObservableCollection<SheetSummary> SheetList { get; } = new();
|
||
|
||
/// <summary>서식 목록 검색어</summary>
|
||
public string SheetSearchKeyword
|
||
{
|
||
get => sheetSearchKeyword;
|
||
set => SetProperty(ref sheetSearchKeyword, value);
|
||
}
|
||
|
||
/// <summary>서식 목록 로딩 중</summary>
|
||
public bool IsSheetListLoading
|
||
{
|
||
get => isSheetListLoading;
|
||
set
|
||
{
|
||
if (SetProperty(ref isSheetListLoading, value))
|
||
{
|
||
OnPropertyChanged(nameof(SheetListStatusText));
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 서식 목록 하단 상태 한 줄 — 로딩·건수·미접속을 목록 바로 아래에서 알린다.
|
||
/// 종전에는 목록 위에 "불러오는 중..." 오버레이를 덮어 목록이 가려졌고, 건수는 창 하단
|
||
/// 전역 상태줄에만 떠서 목록과 시선 거리가 멀었다.
|
||
/// </summary>
|
||
public string SheetListStatusText
|
||
{
|
||
get
|
||
{
|
||
if (!CanUseDb)
|
||
{
|
||
return "DB 미접속 — 파일 모드";
|
||
}
|
||
if (IsSheetListLoading)
|
||
{
|
||
return "불러오는 중…";
|
||
}
|
||
if (SheetList.Count == 0)
|
||
{
|
||
return "결과 없음";
|
||
}
|
||
var text = sheetGroupCount > 0 ? $"{SheetList.Count}건 · 분류 {sheetGroupCount}종" : $"{SheetList.Count}건";
|
||
if (sheetListTruncated)
|
||
{
|
||
text += $" · 상한 {FormDesignDataBusiness.SheetListLimit}건에 도달해 뒤쪽 분류가 잘렸습니다 — 검색어로 좁히세요";
|
||
}
|
||
// 입력 중인 검색어가 아니라 '적용된' 검색어를 본다 — 타이핑 중 문구가 깜빡이면 안 된다
|
||
if (appliedSheetKeyword.Length > 0)
|
||
{
|
||
text += " (검색 결과 기준)";
|
||
}
|
||
return text;
|
||
}
|
||
}
|
||
|
||
/// <summary>현재 목록에 나타난 분류 수 — 목록 하단 표시용</summary>
|
||
public int SheetGroupCount
|
||
{
|
||
get => sheetGroupCount;
|
||
set
|
||
{
|
||
if (SetProperty(ref sheetGroupCount, value))
|
||
{
|
||
OnPropertyChanged(nameof(SheetListStatusText));
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 서식 목록 분류 그룹 전체 펼치기/접기 — 목록 헤더 버튼.
|
||
/// 선택을 보존한다: 레거시는 서식이 꽉 찬 평면 목록으로 시작하므로 접힌 첫 화면을 낯설어하는
|
||
/// 사용자가 있다. 한 번 펴 두면 다음 실행에도 그대로 열려 있어야 매번 다시 펴지 않는다.
|
||
/// </summary>
|
||
public bool AreSheetGroupsExpanded
|
||
{
|
||
get => areSheetGroupsExpanded;
|
||
set
|
||
{
|
||
if (SetProperty(ref areSheetGroupsExpanded, value))
|
||
{
|
||
SheetGroupRegistry.SetAllExpanded(value);
|
||
UserPrefs.SetBool(SheetGroupsExpandedPref, value);
|
||
OnPropertyChanged(nameof(SheetGroupToggleIcon));
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>전체 펼치기/접기 버튼 아이콘</summary>
|
||
public string SheetGroupToggleIcon => areSheetGroupsExpanded ? "chevrons-down-up" : "chevrons-up-down";
|
||
|
||
/// <summary>열린 문서 존재 여부 — 빈 상태 오버레이 표시 판정</summary>
|
||
public bool HasOpenDocuments => OpenDesigners.Count > 0;
|
||
|
||
/// <summary>DB 사용 가능 여부(서식 목록 패널 안내용)</summary>
|
||
public bool CanUseDb => dataBusiness.CanUseDb;
|
||
|
||
/// <summary>좌측 패널 탭(0=서식 목록, 1=레이어, 2=도구 상자) — 서식 열면 레이어로 자동 전환([200] 관행)</summary>
|
||
public int SelectedLeftTabIndex
|
||
{
|
||
get => selectedLeftTabIndex;
|
||
set => SetProperty(ref selectedLeftTabIndex, value);
|
||
}
|
||
|
||
/// <summary>손(팬) 도구 활성 — 캔버스 드래그로 화면 이동(false=선택 도구, [200] 플로팅 바 관행)</summary>
|
||
public bool IsHandTool
|
||
{
|
||
get => isHandTool;
|
||
set => SetProperty(ref isHandTool, value);
|
||
}
|
||
#endregion
|
||
|
||
#region Constructors
|
||
public MainViewModel()
|
||
{
|
||
LoadedCommand = new Command(async (sender, e) => await OnLoadedAsync());
|
||
NewFileCommand = new Command((sender, e) => OnNewFile());
|
||
OpenFileCommand = new Command((sender, e) => OnOpenFile());
|
||
SaveFileCommand = new Command((sender, e) => OnSaveFile(saveAs: false));
|
||
SaveAsFileCommand = new Command((sender, e) => OnSaveFile(saveAs: true));
|
||
OpenFromDbCommand = new Command((sender, e) => OnOpenFromDb());
|
||
SaveToDbCommand = new Command((sender, e) => OnSaveToDb());
|
||
ExportJsonCommand = new Command((sender, e) => OnExportJson());
|
||
ImportJsonCommand = new Command((sender, e) => OnImportJson());
|
||
PreviewCommand = new Command((sender, e) => OnPreview());
|
||
PrintCommand = new Command((sender, e) => OnPrint());
|
||
RecordWordCommand = new Command((sender, e) => OnRecordWords());
|
||
FontManagerCommand = new Command((sender, e) => OnFontManager());
|
||
SheetHistoryCommand = new Command((sender, e) => OnSheetHistory());
|
||
ZoomInCommand = new Command((sender, e) => CurrentDesigner?.ZoomIn());
|
||
ZoomOutCommand = new Command((sender, e) => CurrentDesigner?.ZoomOut());
|
||
ZoomResetCommand = new Command((sender, e) => CurrentDesigner?.ZoomReset());
|
||
ZoomFitCommand = new Command((sender, e) => CurrentDesigner?.ZoomFit());
|
||
// Application.Shutdown() 은 Window.Closing 의 e.Cancel 을 무시하므로 VM 에서 먼저 확인한다
|
||
ExitCommand = new Command((sender, e) =>
|
||
{
|
||
if (!ConfirmCloseAll())
|
||
{
|
||
return;
|
||
}
|
||
IsShuttingDown = true;
|
||
Application.Current.Shutdown();
|
||
});
|
||
SearchSheetsCommand = new Command(async (sender, e) => await LoadSheetListAsync());
|
||
OpenSheetCommand = new Command((object param) => OnOpenSheetFromList(param as SheetSummary));
|
||
CloseDocumentCommand = new Command((object param) => OnCloseDocument(param as DesignerViewModel));
|
||
|
||
SheetList.CollectionChanged += (s, e) => OnPropertyChanged(nameof(SheetListStatusText));
|
||
OpenDesigners.CollectionChanged += (s, e) => OnPropertyChanged(nameof(HasOpenDocuments));
|
||
}
|
||
#endregion
|
||
|
||
#region Commands
|
||
/// <summary>창 로드</summary>
|
||
public ICustomCommand? LoadedCommand { get; set; }
|
||
|
||
/// <summary>새 서식</summary>
|
||
public ICustomCommand? NewFileCommand { get; set; }
|
||
|
||
/// <summary>서식 XML 열기</summary>
|
||
public ICustomCommand? OpenFileCommand { get; set; }
|
||
|
||
/// <summary>저장</summary>
|
||
public ICustomCommand? SaveFileCommand { get; set; }
|
||
|
||
/// <summary>다른 이름으로 저장</summary>
|
||
public ICustomCommand? SaveAsFileCommand { get; set; }
|
||
|
||
/// <summary>DB에서 서식 열기(검색 대화상자)</summary>
|
||
public ICustomCommand? OpenFromDbCommand { get; set; }
|
||
|
||
/// <summary>DB에 저장(E_SdgMst 버저닝 + E_SctMst 재생성) — SaveMode 게이트</summary>
|
||
public ICustomCommand? SaveToDbCommand { get; set; }
|
||
|
||
/// <summary>JSON 내보내기</summary>
|
||
public ICustomCommand? ExportJsonCommand { get; set; }
|
||
|
||
/// <summary>JSON 가져오기</summary>
|
||
public ICustomCommand? ImportJsonCommand { get; set; }
|
||
|
||
/// <summary>미리보기</summary>
|
||
public ICustomCommand? PreviewCommand { get; set; }
|
||
|
||
/// <summary>인쇄</summary>
|
||
public ICustomCommand? PrintCommand { get; set; }
|
||
|
||
/// <summary>상용구 관리</summary>
|
||
public ICustomCommand? RecordWordCommand { get; set; }
|
||
|
||
/// <summary>폰트 일괄 변경</summary>
|
||
public ICustomCommand? FontManagerCommand { get; set; }
|
||
|
||
/// <summary>서식 수정이력(버전 열람/복원)</summary>
|
||
public ICustomCommand? SheetHistoryCommand { get; set; }
|
||
|
||
/// <summary>줌 확대</summary>
|
||
public ICustomCommand? ZoomInCommand { get; set; }
|
||
|
||
/// <summary>줌 축소</summary>
|
||
public ICustomCommand? ZoomOutCommand { get; set; }
|
||
|
||
/// <summary>줌 100%</summary>
|
||
public ICustomCommand? ZoomResetCommand { get; set; }
|
||
|
||
/// <summary>화면 맞춤 — 활성 페이지 한 장이 뷰포트에 들어오는 배율</summary>
|
||
public ICustomCommand? ZoomFitCommand { get; set; }
|
||
|
||
/// <summary>종료</summary>
|
||
public ICustomCommand? ExitCommand { get; set; }
|
||
|
||
/// <summary>서식 목록 검색</summary>
|
||
public ICustomCommand? SearchSheetsCommand { get; set; }
|
||
|
||
/// <summary>서식 목록에서 열기(더블클릭)</summary>
|
||
public ICustomCommand? OpenSheetCommand { get; set; }
|
||
|
||
/// <summary>문서 탭 닫기</summary>
|
||
public ICustomCommand? CloseDocumentCommand { get; set; }
|
||
#endregion
|
||
|
||
#region Methods - 문서 탭
|
||
/// <summary>문서를 탭으로 추가하고 활성화 — 좌측 패널은 레이어 탭으로 전환</summary>
|
||
private void AttachDocument(DesignerViewModel designer)
|
||
{
|
||
OpenDesigners.Add(designer);
|
||
CurrentDesigner = designer;
|
||
SelectedLeftTabIndex = 1;
|
||
}
|
||
|
||
private void OnCloseDocument(DesignerViewModel? designer)
|
||
{
|
||
designer ??= CurrentDesigner;
|
||
if (designer is null)
|
||
{
|
||
return;
|
||
}
|
||
if (designer.Undo.IsDirty
|
||
&& !DialogService.Confirm("문서 닫기",
|
||
$"'{designer.DisplayName}' 문서에 저장하지 않은 변경이 있습니다.",
|
||
"닫을까요?", yes: "닫기", no: "취소", destructive: true))
|
||
{
|
||
return;
|
||
}
|
||
var index = OpenDesigners.IndexOf(designer);
|
||
OpenDesigners.Remove(designer);
|
||
if (CurrentDesigner == designer)
|
||
{
|
||
CurrentDesigner = OpenDesigners.Count > 0
|
||
? OpenDesigners[Math.Clamp(index, 0, OpenDesigners.Count - 1)]
|
||
: null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 종료 전 미저장 문서 처리 — 진행 가능하면 true, 사용자가 한 곳에서라도 취소하면 false.
|
||
/// 문서별로 확인하며, 확인 직전 해당 문서를 활성 탭으로 올려 어느 문서를 묻는지 보이게 한다
|
||
/// (저장 경로도 CurrentDesigner 를 대상으로 동작하므로 전환이 필수다).
|
||
/// </summary>
|
||
public bool ConfirmCloseAll()
|
||
{
|
||
foreach (var designer in OpenDesigners.ToList())
|
||
{
|
||
if (!designer.Undo.IsDirty)
|
||
{
|
||
continue;
|
||
}
|
||
CurrentDesigner = designer;
|
||
|
||
// 과거 버전 열람 탭은 저장하면 활성 디자인을 과거 내용으로 덮게 되므로 저장 선택지를 주지 않는다
|
||
if (designer.HistorySdgKey is not null)
|
||
{
|
||
if (!DialogService.Confirm("종료",
|
||
$"'{designer.DisplayName}' 은(는) 과거 버전 열람 탭이라 저장할 수 없습니다.",
|
||
"변경을 버리고 종료할까요?", yes: "버리고 종료", no: "취소", destructive: true))
|
||
{
|
||
return false;
|
||
}
|
||
continue;
|
||
}
|
||
|
||
var answer = DialogService.ConfirmWithCancel("종료",
|
||
$"'{designer.DisplayName}' 문서에 저장하지 않은 변경이 있습니다.",
|
||
"저장할까요?", yes: "저장", no: "저장 안 함");
|
||
if (answer is null)
|
||
{
|
||
return false;
|
||
}
|
||
if (answer == false)
|
||
{
|
||
continue;
|
||
}
|
||
if (!(designer.IsFromDb ? OnSaveToDb() : OnSaveFile(saveAs: false)))
|
||
{
|
||
return false; // 저장 대화상자 취소 또는 오류 — 종료를 중단한다
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/// <summary>종료 확인을 이미 통과했는지 — MainView.Closing 이 재질문하지 않도록</summary>
|
||
public bool IsShuttingDown { get; private set; }
|
||
|
||
/// <summary>
|
||
/// 이미 열려 있는 같은 서식 — 없으면 null.
|
||
///
|
||
/// <b>출처(파일/DB)를 보지 않는다.</b> 레거시 MultiSheetFormAllow 도 탭 제목의 [서식코드]만 보고
|
||
/// 출처와 무관하게 막는다(frmSheetDesigner.vb:242-252). 출처를 따지면 XML 파일로 열어 둔 서식을
|
||
/// 목록에서 다시 DB 로 열 수 있고, 두 탭에서 교대로 저장하면 <b>마지막 저장이 앞선 편집을 통째로 덮는다</b>
|
||
/// — 양쪽 어디에도 낙관적 충돌 검출이 없다.
|
||
///
|
||
/// 이력본(HistorySdgKey 가 있는 읽기 전용 스냅샷)은 제외한다. 같은 ShtCod 를 갖고 있어서
|
||
/// 걸러내지 않으면 현재본을 열려는 요청이 옛 이력 탭을 활성화하고, 사용자는 현재본을
|
||
/// 편집한다고 믿은 채 스냅샷을 고치게 된다.
|
||
///
|
||
/// 서식 코드가 비어 있으면(새 서식) 서로 다른 문서로 본다 — 빈 코드끼리 묶으면
|
||
/// '새 서식'을 두 번 만들 수 없다.
|
||
/// </summary>
|
||
internal static DesignerViewModel? FindOpenSheet(IEnumerable<DesignerViewModel> open, string formId)
|
||
=> formId.Length == 0
|
||
? null
|
||
: open.FirstOrDefault(d =>
|
||
d.HistorySdgKey is null
|
||
&& string.Equals(d.Document.FormId, formId, StringComparison.OrdinalIgnoreCase));
|
||
|
||
/// <summary>
|
||
/// 이미 열려 있으면 그 탭을 활성화하고 true — 새로 열지 말라는 뜻.
|
||
/// 설정으로 중복 열기를 허용하면 항상 false(레거시 AllowMultiSheetForm 과 같은 탈출구).
|
||
/// </summary>
|
||
private bool ActivateIfAlreadyOpen(string formId, string source)
|
||
{
|
||
if (dataBusiness.Config.AllowMultiSheetForm)
|
||
{
|
||
return false;
|
||
}
|
||
var existing = FindOpenSheet(OpenDesigners, formId);
|
||
if (existing is null)
|
||
{
|
||
return false;
|
||
}
|
||
CurrentDesigner = existing;
|
||
// 어느 탭으로 갔는지 알려 준다 — 아무 일도 안 일어난 것처럼 보이면 사용자는 다시 누른다
|
||
var where = existing.IsFromDb ? "DB 탭" : "파일 탭";
|
||
StatusText = $"이미 열린 서식: {formId} — {where}으로 이동했습니다({source} 열기는 취소).";
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 서식생성기 사용 권한 확인(E_ShtMst.ShtUsrDesYon) — 레거시 DisplaySheetDesign 게이트 이식.
|
||
/// 거부 메시지는 레거시 문구를 그대로 재현한다(사용자 재교육 비용 0, 조치 안내가 이미 담겨 있다).
|
||
/// </summary>
|
||
private bool EnsureDesignAllowed(string shtCod)
|
||
{
|
||
try
|
||
{
|
||
switch (dataBusiness.GetDesignPermission(shtCod))
|
||
{
|
||
case SheetDesignPermission.Allowed:
|
||
return true;
|
||
case SheetDesignPermission.NotRegistered:
|
||
DialogService.Notify(DialogKind.Warning, "확인",
|
||
$"기록지 정보에 등록되지 않은 서식 코드입니다. ({shtCod})");
|
||
return false;
|
||
default:
|
||
Services.AppLog.Audit($"[권한거부] {shtCod} — ShtUsrDesYon 차단 (by {dataBusiness.User.Display})");
|
||
DialogService.Notify(DialogKind.Warning, "확인",
|
||
"서식생성기를 사용하지 않는 서식지입니다.", "기록지 정보를 확인해주세요.");
|
||
return false;
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
DialogService.Notify(DialogKind.Warning, "확인", "서식 권한을 확인하지 못했습니다.", ex.Message);
|
||
return false;
|
||
}
|
||
}
|
||
#endregion
|
||
|
||
#region Methods - 열기/저장
|
||
/// <summary>기동 인자로 받은 서식 코드 — 창 로드 후 자동으로 연다(App.OnStartup 이 세팅)</summary>
|
||
public string? PendingSheetCode { get; set; }
|
||
|
||
private async Task OnLoadedAsync()
|
||
{
|
||
await LoadSheetListAsync();
|
||
|
||
// 기동 인자로 서식을 지정했으면 빈 새 문서를 만들지 않는다 — 레거시도 해당 서식만 연다.
|
||
// 열기에 실패하면(디자인 없음/권한 거부/코드 오타) 빈 문서로 폴백해 앱이 빈 껍데기가 되지 않게 한다.
|
||
if (PendingSheetCode is { Length: > 0 } shtCod)
|
||
{
|
||
PendingSheetCode = null;
|
||
OpenDbSheet(shtCod);
|
||
if (OpenDesigners.Count > 0)
|
||
{
|
||
return;
|
||
}
|
||
}
|
||
|
||
OnNewFile();
|
||
SelectedLeftTabIndex = 0; // 시작 화면은 서식 목록 탭(초기 빈 문서로 레이어 탭 전환되는 것 되돌림)
|
||
}
|
||
|
||
private void OnNewFile()
|
||
{
|
||
try
|
||
{
|
||
AttachDocument(new DesignerViewModel(dataBusiness.CreateNew()));
|
||
StatusText = "새 서식 (720×856)";
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
DialogService.ShowError("새 서식 만들기", ex);
|
||
}
|
||
}
|
||
|
||
private void OnOpenFile()
|
||
{
|
||
try
|
||
{
|
||
var path = dialogService.ShowOpenFile(XmlFilter, "서식 XML 열기");
|
||
if (path is null)
|
||
{
|
||
return;
|
||
}
|
||
var document = dataBusiness.OpenXmlFile(path);
|
||
// 같은 서식이 DB 탭으로 이미 열려 있을 수 있다 — 출처가 달라도 저장 대상은 같은 서식이다
|
||
if (ActivateIfAlreadyOpen(document.FormId, "파일"))
|
||
{
|
||
return;
|
||
}
|
||
AttachDocument(new DesignerViewModel(document) { FilePath = path });
|
||
StatusText = $"파일 로드: 페이지 {document.Pages.Count} · 컨트롤 {document.Pages.Sum(p => CountControls(p.Controls))}";
|
||
ShowReadWarnings(document.Meta.ReadWarnings, "서식 열기");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
DialogService.ShowError("서식 열기", ex);
|
||
}
|
||
}
|
||
|
||
/// <summary>파일 저장 — 성공 시 true. 대화상자 취소·오류는 false(종료 가드가 이 값으로 진행 여부를 판단한다)</summary>
|
||
private bool OnSaveFile(bool saveAs)
|
||
{
|
||
try
|
||
{
|
||
if (CurrentDesigner is null)
|
||
{
|
||
return false;
|
||
}
|
||
CommitPendingEdits();
|
||
var path = CurrentDesigner.FilePath;
|
||
if (saveAs || path is null)
|
||
{
|
||
path = dialogService.ShowSaveFile(XmlFilter, "서식 XML 저장",
|
||
CurrentDesigner.Document.FormId + ".xml");
|
||
if (path is null)
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
dataBusiness.SaveXmlFile(CurrentDesigner.Document, path);
|
||
CurrentDesigner.FilePath = path;
|
||
CurrentDesigner.Undo.MarkSaved();
|
||
CurrentDesigner.NotifyDisplayNameChanged();
|
||
Title = $"SheetMe 서식생성기 — {CurrentDesigner.DisplayName}";
|
||
StatusText = $"저장됨: {path}";
|
||
return true;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
DialogService.Notify(DialogKind.Error, "오류", "저장 중 오류가 발생했습니다.", ex.Message);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 포커스가 머물러 있는 편집 중 값을 소스에 반영한다.
|
||
/// 인스펙터 바인딩이 UpdateSourceTrigger=LostFocus 라, 값을 고치다 바로 Ctrl+S 를 누르면
|
||
/// 커밋되지 않은 채 저장되어 조용히 유실된다.
|
||
/// </summary>
|
||
private static void CommitPendingEdits()
|
||
{
|
||
if (System.Windows.Input.Keyboard.FocusedElement is System.Windows.Controls.TextBox box)
|
||
{
|
||
box.GetBindingExpression(System.Windows.Controls.TextBox.TextProperty)?.UpdateSource();
|
||
}
|
||
}
|
||
|
||
private void OnOpenFromDb()
|
||
{
|
||
try
|
||
{
|
||
if (!EnsureDb())
|
||
{
|
||
return;
|
||
}
|
||
var dialog = new Views.SheetOpenDialogView(keyword => dataBusiness.ListSheets(keyword))
|
||
{
|
||
Owner = Application.Current.MainWindow,
|
||
};
|
||
if (dialog.ShowDialog() != true || dialog.SelectedSheet is null)
|
||
{
|
||
return;
|
||
}
|
||
OpenDbSheet(dialog.SelectedSheet.ShtCod);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
DialogService.Notify(DialogKind.Error, "오류", "DB에서 서식을 여는 중 오류가 발생했습니다.", ex.Message);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// DB 서식 열기 공통 — 이미 열려 있으면 탭 활성화.
|
||
/// 모든 열기 경로(대화상자·목록 더블클릭·기동 인자)가 여기로 수렴하므로 권한 게이트를 여기 둔다.
|
||
/// 레거시는 게이트가 진입점마다 흩어져 있어 이력 노드 클릭·MessageQueue 두 경로로 우회가 가능했다.
|
||
/// </summary>
|
||
private void OpenDbSheet(string shtCod)
|
||
{
|
||
if (ActivateIfAlreadyOpen(shtCod, "DB"))
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (!EnsureDesignAllowed(shtCod))
|
||
{
|
||
return;
|
||
}
|
||
|
||
var document = dataBusiness.OpenFromDb(shtCod);
|
||
if (document is null)
|
||
{
|
||
DialogService.Notify(DialogKind.Warning, "DB 열기", "활성 디자인을 찾지 못했습니다.");
|
||
return;
|
||
}
|
||
|
||
AttachDocument(new DesignerViewModel(document, dataBusiness.LoadSpreadGrids(shtCod)) { IsFromDb = true });
|
||
StatusText = $"DB 로드: {document.FormId} (SdgKey {document.Meta.SourceSdgKey}) · " +
|
||
$"페이지 {document.Pages.Count} · 컨트롤 {document.Pages.Sum(p => CountControls(p.Controls))}";
|
||
ShowReadWarnings(document.Meta.ReadWarnings, "DB 열기");
|
||
}
|
||
|
||
/// <summary>DB 저장 — 성공 시 true. 게이트 차단·취소·오류는 false</summary>
|
||
private bool OnSaveToDb()
|
||
{
|
||
try
|
||
{
|
||
if (CurrentDesigner is null)
|
||
{
|
||
return false;
|
||
}
|
||
if (!dataBusiness.CanSaveToDb)
|
||
{
|
||
DialogService.Notify(DialogKind.Warning, "DB 저장",
|
||
"DB 저장이 비활성화되어 있습니다.",
|
||
"appsettings.json 의 FormStore:SaveMode 를 'Db' 로 설정한 뒤 사용하세요.\n" +
|
||
"(운영 레거시 테이블에 기록되므로 명시적 활성화가 필요합니다)");
|
||
return false;
|
||
}
|
||
if (!dataBusiness.CanWriteDb)
|
||
{
|
||
DialogService.Notify(DialogKind.Warning, "DB 저장",
|
||
"HIS 사용자가 확인되지 않아 DB에 저장할 수 없습니다.",
|
||
"기록지정보 화면에서 서식생성기를 실행하거나, 사용자 코드를 인자로 전달해 주세요.\n" +
|
||
"(저장 이력에 남길 수정자를 특정할 수 없습니다)");
|
||
return false;
|
||
}
|
||
CommitPendingEdits();
|
||
|
||
var document = CurrentDesigner.Document;
|
||
|
||
// 열기 이후 마스터에서 플래그가 꺼졌을 수 있고, 파일→DB 저장은 열기 게이트를 아예 거치지 않는다.
|
||
// 레거시는 저장 경로에 게이트가 없어 이 구멍이 열려 있었다.
|
||
if (dataBusiness.SheetExists(document.FormId) && !EnsureDesignAllowed(document.FormId))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
// 미등록 서식이면 신규 등록 다이얼로그
|
||
if (!dataBusiness.SheetExists(document.FormId))
|
||
{
|
||
var register = new Views.RegisterSheetDialogView(
|
||
document.FormId == "NewSheet" ? string.Empty : document.FormId,
|
||
document.Title == "새 서식" ? string.Empty : document.Title)
|
||
{
|
||
Owner = Application.Current.MainWindow,
|
||
};
|
||
if (register.ShowDialog() != true)
|
||
{
|
||
return false;
|
||
}
|
||
dataBusiness.RegisterSheet(register.SheetCode, register.SheetName, register.ClassCode);
|
||
document.FormId = register.SheetCode;
|
||
document.Title = register.SheetName;
|
||
}
|
||
|
||
var confirm = DialogService.Confirm("DB 저장 확인",
|
||
$"서식 [{document.FormId}] {document.Title} 을(를) DB(E_SdgMst/E_SctMst)에 저장할까요?",
|
||
"기존 활성 디자인은 이력(SdgDelYon='Y')으로 보존되고 새 버전이 생성됩니다.\n" +
|
||
"(제자리 갱신 서식(ShtCneYon='Y')은 기존 버전이 갱신됩니다)",
|
||
yes: "저장", no: "취소");
|
||
if (!confirm)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
var sdgKey = dataBusiness.SaveToDb(document);
|
||
Services.AppLog.Audit($"[DB저장] {document.FormId} → SdgKey {sdgKey} (by {dataBusiness.User.Display})");
|
||
CurrentDesigner.IsFromDb = true;
|
||
// 이력 표식을 지운다 — 저장한 순간 이 탭은 과거 스냅샷이 아니라 활성 디자인이다.
|
||
// 남겨 두면 탭이 '(이력 N)' 으로 계속 보이고, 종료 확인이 '저장할 수 없는 탭' 분기로 들어가
|
||
// 저장 선택지 없이 '변경을 버리고 종료할까요?' 만 물어 이후 편집분이 조용히 버려진다.
|
||
var wasHistory = CurrentDesigner.HistorySdgKey is not null;
|
||
CurrentDesigner.HistorySdgKey = null;
|
||
CurrentDesigner.Undo.MarkSaved();
|
||
CurrentDesigner.NotifyDisplayNameChanged();
|
||
// 이력 탭을 되살린 경우, 같은 서식의 다른 탭은 이제 옛 내용을 들고 있다.
|
||
// 그 탭에서 저장하면 방금 저장한 내용을 덮으므로 어느 탭이 낡았는지 알려 준다.
|
||
if (wasHistory && OpenDesigners.Any(d => !ReferenceEquals(d, CurrentDesigner)
|
||
&& d.HistorySdgKey is null
|
||
&& string.Equals(d.Document.FormId, document.FormId, StringComparison.OrdinalIgnoreCase)))
|
||
{
|
||
DialogService.Notify(DialogKind.Warning, "같은 서식의 다른 탭",
|
||
$"서식 [{document.FormId}] 이(가) 다른 탭에도 열려 있습니다.",
|
||
"그 탭은 방금 저장하기 전의 내용입니다. 거기서 저장하면 이번 저장이 덮어써집니다 — 닫고 다시 여세요.");
|
||
}
|
||
StatusText = $"DB 저장 완료: {document.FormId} → SdgKey {sdgKey}";
|
||
DialogService.Notify(DialogKind.Info, "DB 저장",
|
||
$"저장되었습니다. (SdgKey {sdgKey})", "레거시 뷰어/디자이너에서 열어 확인하세요.");
|
||
return true;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
DialogService.Notify(DialogKind.Error, "오류",
|
||
"DB 저장 중 오류가 발생했습니다. (트랜잭션 롤백됨)", ex.Message);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
private void OnExportJson()
|
||
{
|
||
try
|
||
{
|
||
if (CurrentDesigner is null)
|
||
{
|
||
return;
|
||
}
|
||
var path = dialogService.ShowSaveFile("서식 JSON (*.json)|*.json", "JSON 내보내기",
|
||
CurrentDesigner.Document.FormId + ".json");
|
||
if (path is null)
|
||
{
|
||
return;
|
||
}
|
||
var json = new Core.Serialization.FormJsonSerializer().Write(CurrentDesigner.Document);
|
||
File.WriteAllText(path, json, System.Text.Encoding.UTF8);
|
||
StatusText = $"JSON 내보내기 완료: {path}";
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
DialogService.Notify(DialogKind.Error, "오류", "JSON 내보내기 중 오류가 발생했습니다.", ex.Message);
|
||
}
|
||
}
|
||
|
||
private void OnImportJson()
|
||
{
|
||
try
|
||
{
|
||
var path = dialogService.ShowOpenFile("서식 JSON (*.json)|*.json", "JSON 가져오기");
|
||
if (path is null)
|
||
{
|
||
return;
|
||
}
|
||
var document = new Core.Serialization.FormJsonSerializer().Read(File.ReadAllText(path));
|
||
if (document.FormId.Length == 0)
|
||
{
|
||
document.FormId = Path.GetFileNameWithoutExtension(path);
|
||
}
|
||
if (ActivateIfAlreadyOpen(document.FormId, "JSON"))
|
||
{
|
||
return;
|
||
}
|
||
AttachDocument(new DesignerViewModel(document));
|
||
StatusText = $"JSON 로드: 페이지 {document.Pages.Count} · 컨트롤 {document.Pages.Sum(p => CountControls(p.Controls))}";
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
DialogService.Notify(DialogKind.Error, "오류", "JSON 가져오기 중 오류가 발생했습니다.", ex.Message);
|
||
}
|
||
}
|
||
#endregion
|
||
|
||
#region Methods - 서식 목록 패널
|
||
/// <summary>서식 목록 로드(비동기) — DB 미접속이면 건너뜀</summary>
|
||
private async Task LoadSheetListAsync()
|
||
{
|
||
if (!dataBusiness.CanUseDb || IsSheetListLoading)
|
||
{
|
||
return;
|
||
}
|
||
try
|
||
{
|
||
IsSheetListLoading = true;
|
||
var keyword = SheetSearchKeyword;
|
||
var sheets = await Task.Run(() => dataBusiness.ListSheets(keyword));
|
||
|
||
// 그룹 라벨은 목록에 담기 전에 등록해야 한다 — 나중에 바꾸면 이미 만들어진 헤더가 코드로 남는다
|
||
SheetGroupRegistry.Register(sheets);
|
||
SheetList.Clear();
|
||
foreach (var sheet in sheets)
|
||
{
|
||
SheetList.Add(sheet);
|
||
}
|
||
// 검색 중이면 결과가 접힌 헤더 뒤에 숨지 않도록 전부 펼친다. 검색어를 지우면 사용자가
|
||
// 보존해 둔 취향으로 되돌아간다. 새로 생긴 그룹은 기본 접힘이라 값이 같아도 매번 밀어 넣어야
|
||
// 토글 버튼 표시와 어긋나지 않는다.
|
||
var expand = keyword.Trim().Length > 0 || UserPrefs.GetBool(SheetGroupsExpandedPref, false);
|
||
SheetGroupRegistry.SetAllExpanded(expand);
|
||
areSheetGroupsExpanded = expand;
|
||
OnPropertyChanged(nameof(AreSheetGroupsExpanded));
|
||
OnPropertyChanged(nameof(SheetGroupToggleIcon));
|
||
|
||
// 상한에 닿았다는 건 뒤쪽 분류가 통째로 잘렸다는 뜻이다 — 조용히 넘기면 "그 분류가 없다"로 읽힌다
|
||
sheetListTruncated = sheets.Count >= FormDesignDataBusiness.SheetListLimit;
|
||
appliedSheetKeyword = keyword.Trim();
|
||
SheetGroupCount = sheets.Select(s => s.ClassCode).Distinct(StringComparer.Ordinal).Count();
|
||
StatusText = $"서식 목록 {sheets.Count}건 · 분류 {SheetGroupCount}종";
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
DialogService.Notify(DialogKind.Error, "서식 목록", "서식 목록 조회 중 오류가 발생했습니다.", ex.Message);
|
||
}
|
||
finally
|
||
{
|
||
IsSheetListLoading = false;
|
||
}
|
||
}
|
||
|
||
/// <summary>서식 목록에서 열기 — 뷰 더블클릭 핸들러가 위임 호출</summary>
|
||
public void OpenSheetFromList(SheetSummary? sheet) => OnOpenSheetFromList(sheet);
|
||
|
||
private void OnOpenSheetFromList(SheetSummary? sheet)
|
||
{
|
||
try
|
||
{
|
||
if (sheet is null)
|
||
{
|
||
return;
|
||
}
|
||
if (!sheet.HasDesign)
|
||
{
|
||
DialogService.Notify(DialogKind.Warning, "서식 열기", "선택한 서식에는 저장된 디자인이 없습니다.");
|
||
return;
|
||
}
|
||
OpenDbSheet(sheet.ShtCod);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
DialogService.Notify(DialogKind.Error, "오류", "서식을 여는 중 오류가 발생했습니다.", ex.Message);
|
||
}
|
||
}
|
||
#endregion
|
||
|
||
#region Methods - 도구
|
||
private void OnPreview()
|
||
{
|
||
if (CurrentDesigner is null)
|
||
{
|
||
return;
|
||
}
|
||
new Views.PreviewWindow(CurrentDesigner)
|
||
{
|
||
Owner = Application.Current.MainWindow,
|
||
}.Show();
|
||
}
|
||
|
||
private void OnPrint()
|
||
{
|
||
try
|
||
{
|
||
if (CurrentDesigner is null)
|
||
{
|
||
return;
|
||
}
|
||
Services.PrintService.Print(CurrentDesigner, CurrentDesigner.Document.Title.Length > 0
|
||
? CurrentDesigner.Document.Title
|
||
: CurrentDesigner.Document.FormId);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
DialogService.Notify(DialogKind.Error, "오류", "인쇄 중 오류가 발생했습니다.", ex.Message);
|
||
}
|
||
}
|
||
|
||
private void OnFontManager()
|
||
{
|
||
if (CurrentDesigner is null)
|
||
{
|
||
return;
|
||
}
|
||
new Views.FontManagerDialogView(CurrentDesigner)
|
||
{
|
||
Owner = Application.Current.MainWindow,
|
||
}.ShowDialog();
|
||
}
|
||
|
||
private void OnSheetHistory()
|
||
{
|
||
try
|
||
{
|
||
if (CurrentDesigner is null || !EnsureDb())
|
||
{
|
||
return;
|
||
}
|
||
var document = CurrentDesigner.Document;
|
||
if (document.FormId.Length == 0 || document.FormId == "NewSheet")
|
||
{
|
||
DialogService.Notify(DialogKind.Warning, "서식 수정이력", "수정이력은 DB에 저장된 서식에서 사용할 수 있습니다.");
|
||
return;
|
||
}
|
||
|
||
// 파일에서 연 문서의 FormId 가 우연히 거부 서식 코드와 같으면 이 경로로 이력 열람·복원이
|
||
// 가능해진다(레거시의 우회 경로 중 하나였다).
|
||
if (!EnsureDesignAllowed(document.FormId))
|
||
{
|
||
return;
|
||
}
|
||
|
||
var versions = dataBusiness.ListVersions(document.FormId);
|
||
if (versions.Count == 0)
|
||
{
|
||
DialogService.Notify(DialogKind.Info, "서식 수정이력", "저장된 버전이 없습니다.");
|
||
return;
|
||
}
|
||
|
||
var dialog = new Views.SheetHistoryDialogView(document.FormId, document.Title, versions)
|
||
{
|
||
Owner = Application.Current.MainWindow,
|
||
};
|
||
if (dialog.ShowDialog() != true || dialog.SelectedSdgKey is not { } sdgKey)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// 이미 열람 중인 동일 버전 탭이면 활성화
|
||
var existing = OpenDesigners.FirstOrDefault(d => d.HistorySdgKey == sdgKey
|
||
&& d.Document.FormId == document.FormId);
|
||
if (existing is not null)
|
||
{
|
||
CurrentDesigner = existing;
|
||
return;
|
||
}
|
||
|
||
var versionDocument = dataBusiness.OpenFromDbVersion(document.FormId, sdgKey);
|
||
if (versionDocument is null)
|
||
{
|
||
DialogService.Notify(DialogKind.Warning, "서식 수정이력", "해당 버전을 불러오지 못했습니다.");
|
||
return;
|
||
}
|
||
|
||
AttachDocument(new DesignerViewModel(versionDocument, dataBusiness.LoadSpreadGrids(document.FormId))
|
||
{
|
||
IsFromDb = true,
|
||
HistorySdgKey = sdgKey,
|
||
});
|
||
StatusText = $"이력 열람: {document.FormId} SdgKey {sdgKey} — 'DB에 저장' 시 이 내용이 새 활성 버전이 됩니다";
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
DialogService.Notify(DialogKind.Error, "오류", "수정이력 조회 중 오류가 발생했습니다.", ex.Message);
|
||
}
|
||
}
|
||
|
||
private void OnRecordWords()
|
||
{
|
||
try
|
||
{
|
||
if (CurrentDesigner is null)
|
||
{
|
||
return;
|
||
}
|
||
if (!EnsureDb())
|
||
{
|
||
return;
|
||
}
|
||
var document = CurrentDesigner.Document;
|
||
if (document.FormId.Length == 0 || document.FormId == "NewSheet")
|
||
{
|
||
DialogService.Notify(DialogKind.Warning, "상용구 관리",
|
||
"상용구는 서식 코드 단위로 저장됩니다.", "먼저 DB에 저장(서식 등록)한 뒤 사용하세요.");
|
||
return;
|
||
}
|
||
|
||
new Views.RecordWordDialogView(dataBusiness.RecordWords(), document.FormId, document.Title,
|
||
dataBusiness.User.UidCod, dataBusiness.CanWriteDb)
|
||
{
|
||
Owner = Application.Current.MainWindow,
|
||
}.ShowDialog();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
DialogService.Notify(DialogKind.Error, "오류", "상용구 관리 중 오류가 발생했습니다.", ex.Message);
|
||
}
|
||
}
|
||
#endregion
|
||
|
||
#region Methods - Private
|
||
private bool EnsureDb()
|
||
{
|
||
if (dataBusiness.CanUseDb)
|
||
{
|
||
return true;
|
||
}
|
||
DialogService.Notify(DialogKind.Warning, "DB 연결",
|
||
"DB 접속 문자열(ConnectionStrings:His)이 설정되지 않았습니다.", "appsettings.json 을 확인하세요.");
|
||
return false;
|
||
}
|
||
|
||
private static void ShowReadWarnings(List<string> warnings, string caption)
|
||
{
|
||
if (warnings.Count == 0)
|
||
{
|
||
return;
|
||
}
|
||
var summary = string.Join("\n", warnings.Take(20));
|
||
var more = warnings.Count > 20 ? $"\n... 외 {warnings.Count - 20}건" : string.Empty;
|
||
DialogService.Notify(DialogKind.Warning, caption,
|
||
$"읽기 경고 {warnings.Count}건 (미지원 컨트롤은 자리표시로 보존됩니다)", summary + more);
|
||
}
|
||
|
||
private static int CountControls(List<Core.Models.ControlElement> controls)
|
||
=> controls.Sum(c => 1 + CountControls(c.Children));
|
||
#endregion
|
||
}
|