표(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:
Msystech
2026-08-11 17:45:53 +09:00
co-authored by Claude Fable 5
parent d0538ef95a
commit 3dcac31a5b
3 changed files with 85 additions and 4 deletions
@@ -909,7 +909,7 @@ public sealed class DesignerViewModel : ViewModelBase
clipboard.AddRange(ordered);
}
/// <summary>붙여넣기 — 활성 페이지에 +12,+12 오프셋, 새 Id 부여</summary>
/// <summary>붙여넣기 — 활성 페이지에 +12,+12 오프셋, 새 Id 부여 (표는 제외 — <see cref="ContainsSpread"/> 참조)</summary>
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<ControlViewModel>();
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());
}
/// <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>
public void ReorderSelection(bool toFront)
{