diff --git a/src/SheetMe.Designer/Diagnostics/EditSmoke.cs b/src/SheetMe.Designer/Diagnostics/EditSmoke.cs index 4b288f6..79dcbec 100644 --- a/src/SheetMe.Designer/Diagnostics/EditSmoke.cs +++ b/src/SheetMe.Designer/Diagnostics/EditSmoke.cs @@ -384,7 +384,31 @@ public static class EditSmoke var fromJson = jsonSerializer.Read(json); Check("JSON 왕복 — XML 동일", serializer.Write(fromJson) == serializer.Write(designer.Document)); - // 22) 쿼리 편집기 — XAML 로드/바인딩과 두 모드(SQL/상용구)를 실제로 생성해 확인 + // 22) Spread 판정 — 복제/이름변경 가드의 근거 술어(가드 자체는 MessageBox 를 띄우므로 무인 검증 불가) + var spreadElement = new Core.Models.ControlElement { Type = "Spread", Id = "Spread1" }; + var panelWithSpread = new Core.Models.ControlElement { Type = "Panel", Id = "Panel9" }; + panelWithSpread.Children.Add(new Core.Models.ControlElement { Type = "Label", Id = "L9" }); + panelWithSpread.Children.Add(spreadElement.Clone()); + var plainPanel = new Core.Models.ControlElement { Type = "Panel", Id = "Panel8" }; + plainPanel.Children.Add(new Core.Models.ControlElement { Type = "Label", Id = "L8" }); + + Check("Spread 판정: 자신", DesignerViewModel.ContainsSpread(spreadElement)); + Check("Spread 판정: 컨테이너 자손", DesignerViewModel.ContainsSpread(panelWithSpread)); + Check("Spread 판정: 무관 컨트롤 false", !DesignerViewModel.ContainsSpread(plainPanel)); + Check("Spread 판정: Placeholder 오탐 없음", + !DesignerViewModel.ContainsSpread(new Core.Models.ControlElement { Type = "Placeholder", Id = "P1" })); + + // 붙여넣기 필터 — 표만 걸러내고 나머지는 통과시켜야 한다(전체 거부는 정상 작업까지 막는다) + var mixed = new[] { plainPanel, spreadElement, panelWithSpread, + new Core.Models.ControlElement { Type = "Label", Id = "L7" } }; + var (pastable, blockedSpreads) = DesignerViewModel.FilterPastable(mixed); + Check("붙여넣기 필터: 표 2개 차단", blockedSpreads == 2, $"차단 {blockedSpreads}"); + Check("붙여넣기 필터: 나머지 2개 통과", pastable.Count == 2, $"통과 {pastable.Count}"); + Check("붙여넣기 필터: 통과분에 표 없음", pastable.All(e => !DesignerViewModel.ContainsSpread(e))); + Check("붙여넣기 필터: 표 없으면 전량 통과", + DesignerViewModel.FilterPastable(new[] { plainPanel }) is { BlockedSpreads: 0, Pastable.Count: 1 }); + + // 23) 쿼리 편집기 — XAML 로드/바인딩과 두 모드(SQL/상용구)를 실제로 생성해 확인 var sqlEditor = new Views.QueryEditorWindow("스모크", "SELECT 1 FROM DUAL"); var variableItems = (sqlEditor.FindName("VariableList") as System.Windows.Controls.ListBox)? .ItemsSource?.Cast().Count() ?? 0; @@ -401,7 +425,7 @@ public static class EditSmoke Check("상용구 모드: 제목에 '쿼리' 없음", !wordEditor.Title.Contains("쿼리"), wordEditor.Title); wordEditor.Close(); - // 23) 연속 Undo 로 빈 문서까지 + // 24) 연속 Undo 로 빈 문서까지 var guard = 0; while (designer.Undo.CanUndo && guard++ < 80) { diff --git a/src/SheetMe.Designer/ViewModels/DesignerViewModel.cs b/src/SheetMe.Designer/ViewModels/DesignerViewModel.cs index 5a61748..2048424 100644 --- a/src/SheetMe.Designer/ViewModels/DesignerViewModel.cs +++ b/src/SheetMe.Designer/ViewModels/DesignerViewModel.cs @@ -909,7 +909,7 @@ public sealed class DesignerViewModel : ViewModelBase clipboard.AddRange(ordered); } - /// 붙여넣기 — 활성 페이지에 +12,+12 오프셋, 새 Id 부여 + /// 붙여넣기 — 활성 페이지에 +12,+12 오프셋, 새 Id 부여 (표는 제외 — 참조) public void Paste() { if (clipboard.Count == 0) @@ -922,11 +922,26 @@ public sealed class DesignerViewModel : ViewModelBase return; } + var (pastable, blocked) = FilterPastable(clipboard); + if (blocked > 0) + { + System.Windows.MessageBox.Show( + $"표(Spread) {blocked}개는 붙여넣지 않았습니다.\n\n" + + "표의 격자 디자인은 서식코드+컨트롤이름으로 별도 테이블(E_SpdMst)에서 조회됩니다.\n" + + "복제본에는 격자가 없어 EMR 에서 해당 서식이 열리지 않습니다.\n" + + "표 추가는 레거시 서식생성기를 사용하세요.", + "붙여넣기", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Warning); + } + if (pastable.Count == 0) + { + return; + } + Undo.Snapshot(); var pasted = new List(); var usedIds = IdGenerator.CollectUsed(document); // 모델 순서(0=최상위) 유지: 역순으로 index 0 삽입 - foreach (var source in Enumerable.Reverse(clipboard)) + foreach (var source in Enumerable.Reverse(pastable)) { var clone = source.Clone(); AssignNewIds(clone, usedIds); @@ -938,6 +953,31 @@ public sealed class DesignerViewModel : ViewModelBase Selection.Set(pasted, pasted.LastOrDefault()); } + /// + /// 표(Spread) 타입 — E_SpdMst 격자를 서식코드+이름으로 조회하므로 이름 변경·복제가 금지된다. + /// VM 형이 아니라 모델 Type 문자열로 판정한다(클립보드가 ControlElement 이고 컨테이너 중첩도 잡아야 함). + /// + public static bool IsSpread(ControlElement element) + => string.Equals(element.Type, "Spread", StringComparison.Ordinal); + + /// 자신 또는 자손에 표(Spread)가 있는지 + public static bool ContainsSpread(ControlElement element) + => IsSpread(element) || element.Children.Any(ContainsSpread); + + /// + /// 붙여넣기 가능 항목과 차단된 표 개수 — 알림과 분리해 무인 검증이 가능하도록 둔 순수 함수. + /// 표의 격자는 서식코드+컨트롤이름으로만 조회되는 E_SpdMst 에 있고 SheetMe 는 그 행을 만들지 못한다. + /// 복제본은 이름이 달라 조회 0건 → 레거시가 빈 파일로 FpSpread.Open 을 시도하다 예외 → + /// LoadObject 에 try/catch 가 없어 최상위로 전파되어 그 서식이 통째로 열리지 않는다. + /// 선택 전체를 막지 않고 표만 걸러내는 이유: "라벨 20개 + 표 1개" 복사 같은 정상 작업까지 막히기 때문. + /// + public static (List Pastable, int BlockedSpreads) FilterPastable(IEnumerable source) + { + var all = source.ToList(); + var pastable = all.Where(element => !ContainsSpread(element)).ToList(); + return (pastable, all.Count - pastable.Count); + } + /// 선택 z-order 변경 — 모델 index 0 = 최상위 / VM 마지막 = 최상위 public void ReorderSelection(bool toFront) { diff --git a/src/SheetMe.Designer/ViewModels/Inspector/InspectorViewModel.cs b/src/SheetMe.Designer/ViewModels/Inspector/InspectorViewModel.cs index 34183a2..eff8eb5 100644 --- a/src/SheetMe.Designer/ViewModels/Inspector/InspectorViewModel.cs +++ b/src/SheetMe.Designer/ViewModels/Inspector/InspectorViewModel.cs @@ -372,11 +372,28 @@ public sealed class InspectorViewModel : ViewModelBase { return; } + + // 표(Spread)의 격자는 E_SpdMst 에서 서식코드+이름으로 조회된다. 이름을 바꾸면 조회 0건 → + // 레거시가 빈 파일로 FpSpread.Open 을 시도하다 예외 → LoadObject 에 try/catch 가 없어 + // 최상위로 전파되어 그 서식이 통째로 열리지 않는다. SheetMe 는 E_SpdMst 행을 만들 수 없으므로 금지한다. + if (DesignerViewModel.IsSpread(vm.Model)) + { + System.Windows.MessageBox.Show( + "표(Spread)는 이름을 바꿀 수 없습니다.\n\n" + + $"격자 디자인이 서식코드+이름('{vm.Id}')으로 별도 테이블(E_SpdMst)에 저장되어 있어,\n" + + "이름을 바꾸면 EMR 에서 이 서식이 열리지 않습니다.", + "이름 변경", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Warning); + Rebuild(); + return; + } + var used = IdGenerator.CollectUsed(designer.Document); used.Remove(vm.Id); if (used.Contains(trimmed)) { System.Windows.MessageBox.Show($"이미 사용 중인 이름입니다: {trimmed}", "이름 변경"); + // 거부했으므로 편집 상자에 남은 잘못된 이름을 원래 값으로 되돌린다 + Rebuild(); return; } vm.Model.Id = trimmed;