표(Spread) 이름 변경·복제 차단 — 레거시에서 서식이 통째로 안 열리는 경로 봉쇄
Spread 격자는 E_SpdMst 에서 SpdShtCod+SpdName 으로만 조회되는데 SheetMe 에는 그 행을 만드는 경로가 없다(SELECT 3곳뿐). 이름이 바뀌거나 복제본이 생기면 조회 0건 → Spread.vb:922-955 가 0바이트 파일을 만들고 FpSpread.Open 이 예외 → bzDesignSheetLoader.LoadObject 에는 try/catch 가 없어 재귀를 타고 :425-427 로 전파되어 Return Nothing. 즉 표가 사라지는 수준이 아니라 그 서식이 아예 열리지 않는다. - 이름 변경: 완전 금지(RenameControl 진입부 가드). 기존 중복 이름 거부 경로도 Rebuild() 를 호출하도록 통일 — 지금까지는 return 만 해서 편집 상자에 잘못된 이름이 남아 있었다. - 붙여넣기: 표만 제외하고 나머지는 통과 + 제외 건수 알림. 전체 거부는 라벨 20개에 표 1개가 섞인 복사 같은 정상 작업까지 막는다. 조기 반환을 Undo.Snapshot() 앞에 둬 빈 스텝이 쌓이지 않게 했다. - 가드는 커맨드 람다가 아니라 Paste() 메서드 본문에 — CanvasKeyboardBehavior 가 커맨드를 우회해 designer.CopySelection()/Paste() 를 직접 호출하므로 키보드 경로도 함께 막힌다. - 잘라내기는 별도 가드 불필요(표 삭제 자체는 안전), 복제는 Copy+Paste 라 자동 차단. - 판정은 VM 형이 아니라 모델 Type 문자열 + 자식 재귀 — 클립보드가 ControlElement 이고 컨테이너에 중첩된 표도 잡아야 한다. 검증: edit-smoke 에 판정·필터 순수 로직 8건 추가(FilterPastable 을 알림과 분리해 무인 검증이 가능하도록 함). 실 UI 프로브로 P062(Spread 실물 보유)를 열어 실제 입력으로 이름 변경을 시도 → 가드 대화상자 발동, 레이어·인스펙터 헤더 모두 Spread1 유지, 확인 후 편집 상자 원복까지 확인. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
d0538ef95a
commit
3dcac31a5b
@@ -384,7 +384,31 @@ public static class EditSmoke
|
|||||||
var fromJson = jsonSerializer.Read(json);
|
var fromJson = jsonSerializer.Read(json);
|
||||||
Check("JSON 왕복 — XML 동일", serializer.Write(fromJson) == serializer.Write(designer.Document));
|
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 sqlEditor = new Views.QueryEditorWindow("스모크", "SELECT 1 FROM DUAL");
|
||||||
var variableItems = (sqlEditor.FindName("VariableList") as System.Windows.Controls.ListBox)?
|
var variableItems = (sqlEditor.FindName("VariableList") as System.Windows.Controls.ListBox)?
|
||||||
.ItemsSource?.Cast<object>().Count() ?? 0;
|
.ItemsSource?.Cast<object>().Count() ?? 0;
|
||||||
@@ -401,7 +425,7 @@ public static class EditSmoke
|
|||||||
Check("상용구 모드: 제목에 '쿼리' 없음", !wordEditor.Title.Contains("쿼리"), wordEditor.Title);
|
Check("상용구 모드: 제목에 '쿼리' 없음", !wordEditor.Title.Contains("쿼리"), wordEditor.Title);
|
||||||
wordEditor.Close();
|
wordEditor.Close();
|
||||||
|
|
||||||
// 23) 연속 Undo 로 빈 문서까지
|
// 24) 연속 Undo 로 빈 문서까지
|
||||||
var guard = 0;
|
var guard = 0;
|
||||||
while (designer.Undo.CanUndo && guard++ < 80)
|
while (designer.Undo.CanUndo && guard++ < 80)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -909,7 +909,7 @@ public sealed class DesignerViewModel : ViewModelBase
|
|||||||
clipboard.AddRange(ordered);
|
clipboard.AddRange(ordered);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>붙여넣기 — 활성 페이지에 +12,+12 오프셋, 새 Id 부여</summary>
|
/// <summary>붙여넣기 — 활성 페이지에 +12,+12 오프셋, 새 Id 부여 (표는 제외 — <see cref="ContainsSpread"/> 참조)</summary>
|
||||||
public void Paste()
|
public void Paste()
|
||||||
{
|
{
|
||||||
if (clipboard.Count == 0)
|
if (clipboard.Count == 0)
|
||||||
@@ -922,11 +922,26 @@ public sealed class DesignerViewModel : ViewModelBase
|
|||||||
return;
|
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();
|
Undo.Snapshot();
|
||||||
var pasted = new List<ControlViewModel>();
|
var pasted = new List<ControlViewModel>();
|
||||||
var usedIds = IdGenerator.CollectUsed(document);
|
var usedIds = IdGenerator.CollectUsed(document);
|
||||||
// 모델 순서(0=최상위) 유지: 역순으로 index 0 삽입
|
// 모델 순서(0=최상위) 유지: 역순으로 index 0 삽입
|
||||||
foreach (var source in Enumerable.Reverse(clipboard))
|
foreach (var source in Enumerable.Reverse(pastable))
|
||||||
{
|
{
|
||||||
var clone = source.Clone();
|
var clone = source.Clone();
|
||||||
AssignNewIds(clone, usedIds);
|
AssignNewIds(clone, usedIds);
|
||||||
@@ -938,6 +953,31 @@ public sealed class DesignerViewModel : ViewModelBase
|
|||||||
Selection.Set(pasted, pasted.LastOrDefault());
|
Selection.Set(pasted, pasted.LastOrDefault());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 표(Spread) 타입 — E_SpdMst 격자를 서식코드+이름으로 조회하므로 이름 변경·복제가 금지된다.
|
||||||
|
/// VM 형이 아니라 모델 Type 문자열로 판정한다(클립보드가 ControlElement 이고 컨테이너 중첩도 잡아야 함).
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsSpread(ControlElement element)
|
||||||
|
=> string.Equals(element.Type, "Spread", StringComparison.Ordinal);
|
||||||
|
|
||||||
|
/// <summary>자신 또는 자손에 표(Spread)가 있는지</summary>
|
||||||
|
public static bool ContainsSpread(ControlElement element)
|
||||||
|
=> IsSpread(element) || element.Children.Any(ContainsSpread);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 붙여넣기 가능 항목과 차단된 표 개수 — 알림과 분리해 무인 검증이 가능하도록 둔 순수 함수.
|
||||||
|
/// 표의 격자는 서식코드+컨트롤이름으로만 조회되는 E_SpdMst 에 있고 SheetMe 는 그 행을 만들지 못한다.
|
||||||
|
/// 복제본은 이름이 달라 조회 0건 → 레거시가 빈 파일로 FpSpread.Open 을 시도하다 예외 →
|
||||||
|
/// LoadObject 에 try/catch 가 없어 최상위로 전파되어 그 서식이 통째로 열리지 않는다.
|
||||||
|
/// 선택 전체를 막지 않고 표만 걸러내는 이유: "라벨 20개 + 표 1개" 복사 같은 정상 작업까지 막히기 때문.
|
||||||
|
/// </summary>
|
||||||
|
public static (List<ControlElement> Pastable, int BlockedSpreads) FilterPastable(IEnumerable<ControlElement> source)
|
||||||
|
{
|
||||||
|
var all = source.ToList();
|
||||||
|
var pastable = all.Where(element => !ContainsSpread(element)).ToList();
|
||||||
|
return (pastable, all.Count - pastable.Count);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>선택 z-order 변경 — 모델 index 0 = 최상위 / VM 마지막 = 최상위</summary>
|
/// <summary>선택 z-order 변경 — 모델 index 0 = 최상위 / VM 마지막 = 최상위</summary>
|
||||||
public void ReorderSelection(bool toFront)
|
public void ReorderSelection(bool toFront)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -372,11 +372,28 @@ public sealed class InspectorViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
return;
|
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);
|
var used = IdGenerator.CollectUsed(designer.Document);
|
||||||
used.Remove(vm.Id);
|
used.Remove(vm.Id);
|
||||||
if (used.Contains(trimmed))
|
if (used.Contains(trimmed))
|
||||||
{
|
{
|
||||||
System.Windows.MessageBox.Show($"이미 사용 중인 이름입니다: {trimmed}", "이름 변경");
|
System.Windows.MessageBox.Show($"이미 사용 중인 이름입니다: {trimmed}", "이름 변경");
|
||||||
|
// 거부했으므로 편집 상자에 남은 잘못된 이름을 원래 값으로 되돌린다
|
||||||
|
Rebuild();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
vm.Model.Id = trimmed;
|
vm.Model.Id = trimmed;
|
||||||
|
|||||||
Reference in New Issue
Block a user