2단계 — 신규 서식에 런타임 설정을 걸 수 있게 (선 3종·공통 속성·키 자동완성)

레거시로는 되는데 SheetMe 로는 못 만들던 것들을 메웠다. 어떤 속성을 올릴지는 취향으로 정하지 않고
--db-props 로 운영 디자인 1,271건 / 컨트롤 164,091개를 세고, 각 컨트롤 .vb 의 필드 선언을 읽어 정했다.

■ 선(Line) — 새로 그은 세로선이 EMR 에서 사라지던 문제

MLine 은 방향을 크기가 아니라 Orientation 으로 정한다(MLine.vb:181-190).
키가 없으면 Horizontal 이라 (0,0)→(Width,0) 을 그리므로, 폭 1·높이 210 으로 세로처럼 만든 선은
디자이너에만 보이고 기록지에서는 1px 점이 된다. 운영 선 32,599개 중 Orientation 보유 12,811개이고
그 중 Vertical 이 12,721개 — 세로선은 예외가 아니라 주류다.

Orientation·BorderWidth·DashStyle 을 인스펙터에 올렸다. 그리고 값만 올리지 않고
레거시 컨트롤이 스스로 지키는 불변식을 함께 옮겼다(LineGeometry):
  · 두께 축은 항상 BorderWidth 다 — MLine.OnResize 가 매번 되돌린다. 그래서 선을 끌어
    두껍게 만들 수 없다. 이걸 허용하면 '세로처럼 보이지만 EMR 에선 점'인 선을 다시 만들 수 있다.
  · 방향 전환은 길이를 보존한 채 축을 바꾼다(레거시 Orientation 세터: Width = Height 후 OnResize).
  · 가로로 되돌리면 Orientation 키를 지운다 — <DefaultValue(Horizontal)> 라 레거시 직렬화기도 생략한다.

--db-lines 를 새로 만들어 확인한 결과 운영 선 32,599개에서 경계 모양과 Orientation 불일치는 0건이다.
레거시가 구조적으로 막아 왔다는 뜻이고, 그래서 캔버스 렌더는 건드리지 않았다(P062 바이트 동일).

■ 공통 런타임 속성 — 서명란·인쇄 제외·필수입력·자동높이·재조회

신규 컨트롤은 Props 가 사실상 비어 있어 '전체 속성(0개)'이었고, 고급 편집도 이미 있는 키만 나열한다.
결과적으로 서명이 필요한 동의서나 점수를 합산하는 평가지를 SheetMe 만으로 새로 만들 수 없었다.
타입별 기술자에 다음을 추가했다(기본값은 각 컨트롤 .vb 필드 선언에서 확인한 값이다):
  PrintOutPut(기본 True) · PreventEditing · IsRequiredValue(No/Yes) · Visible · AutoHeight ·
  ReLoadData 3종 · MPictureBox 의 IsSignature/SignatureIndex.

PrintOutPut 을 True 로 표시하는 것이 중요하다 — 키가 없는 컨트롤이 '인쇄 안 함'으로 보이면
사용자가 껐다 켜는 순간 명시 False 가 기록돼 실제로 인쇄에서 빠진다
(런타임 필터: If Me.Visible = False OrElse mbPrintOutPut = False Then Return False).

■ 감사 권고와 다르게 한 것 3가지 — 근거가 반대였다

1. Score 를 5개 타입에 추가하라는 권고는 따르지 않았다. 실제로 저장값을 읽는 것은
   CheckBox·RadioButton 뿐이다(게터가 mdScore 반환). TextBox·MaskedTextBox 의 Score 게터는
   저장값을 무시하고 Me.Text 를 숫자로 읽으며, CalcBox 는 세터가 오히려 Text 를 덮어쓴다.
   편집기를 붙였으면 사용자는 배점을 걸었다고 믿고 런타임은 무시하는 상태가 된다.
   ComboBox 는 Score 가 아니라 ItemScore("/" 구분, 항목 순서와 1:1)를 노출했다.
   실측이 먼저 신호를 줬다: ComboBox 의 Score 값 분포가 Text 와 정확히 같았다(-×625, ++×22).
2. Button 에는 PrintOutPut 을 붙이지 않았다 — MButton.vb 에 그 속성 자체가 없다.
3. 라벨의 표시 여부는 소문자 visible 이다. Label.vb:209-217 이 <Browsable(False)> Shadows 로
   Control.Visible 을 가리고 직렬화기가 그 그림자를 쓴다. 운영에서도 소문자 58,864건(99.7%)
   대 대문자 196건. 대문자로 쓰면 라벨은 그대로 보인다.

■ '속성 추가' 자동완성

큐레이션 밖의 값을 걸려면 키를 손으로 쳐야 했는데, 철자가 틀려도 경고가 없고
그 서식은 배포된 뒤에야 이상하게 동작한다. 타입별 실사용 키 목록(LegacyPropertyCatalog,
--db-props 집계 기반)을 제안 칩으로 깔았다. 이미 있는 키와 인스펙터가 이미 다루는 키는 빼고,
부분 일치로 좁히며 접두 일치를 앞에 둔다. 자유 입력은 그대로 열어 뒀다 — 제안이지 검증이 아니고
사이트가 추가한 키도 있다.

■ 1단계 잔여분: 선택 밖 컨트롤을 Ctrl+드래그하면 엉뚱한 것이 끌리던 문제

Ctrl/Shift 클릭은 선택을 바꾸지 않고 업에서 토글하므로, 그 상태로 드래그가 시작되면
잡은 컨트롤이 아니라 기존 선택분이 끌려갔다(선택이 비어 있으면 아무것도 안 끌렸다).
드래그가 실제로 시작되는 지점에서 잡은 것을 선택에 넣는다 — 기존 다중선택은 유지한다(업 토글과 같은 규칙).

편집 스모크 14건 추가(선 기하 6 · Ctrl+드래그 2 · 자동완성 5 · 라벨 소문자 함정 1).
회귀: 테스트 124/124, 편집 스모크 183건 실패 0,
DB 왕복 1,271건 diff 0/예외 0, 종이 렌더 P062 바이트 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Msystech
2026-08-13 12:38:37 +09:00
co-authored by Claude Opus 5
parent df1a29afa2
commit 59b0e457fe
10 changed files with 699 additions and 17 deletions
@@ -755,6 +755,71 @@ public static class EditSmoke
designer.Selection.Clear();
}
// 20-5e) 선 기하 — 레거시 MLine 은 두께 축을 BorderWidth 로 강제하고,
// 방향 전환은 길이를 보존한 채 축을 바꾼다. 이걸 안 지키면 디자이너와 EMR 이 다른 그림을 그린다.
{
var beforeLine = designer.Pages[0].Controls.ToList();
designer.AddControlAt("Line", new Point(60, 300));
var line = designer.Pages[0].Controls.Except(beforeLine).Single();
Check("선: 기본은 가로 200×1", Math.Abs(line.Width - 200) < 0.5 && Math.Abs(line.Height - 1) < 0.5,
$"{line.Width}×{line.Height}");
Check("선: 기본에는 Orientation 키가 없다(레거시 DefaultValue 와 동일)",
line.Model.Props.GetText("Orientation") is null);
// 세로로 끌어 늘려도 두께는 되돌아온다 — '세로처럼 보이지만 EMR 에선 점' 상태를 만들 수 없다
line.Height = 210;
SheetMe.Core.Catalog.LineGeometry.NormalizeThickness(line.Model);
Check("선: 두께는 끌어서 못 바꾼다", Math.Abs(line.Height - 1) < 0.5, $"높이 {line.Height}");
// 방향 전환 — 길이 보존
SheetMe.Core.Catalog.LineGeometry.ApplyOrientation(line.Model, vertical: true);
Check("선: 세로 전환은 길이를 보존", Math.Abs(line.Model.Bounds.W - 1) < 0.5
&& Math.Abs(line.Model.Bounds.H - 200) < 0.5,
$"{line.Model.Bounds.W}×{line.Model.Bounds.H}");
Check("선: 세로면 Orientation=Vertical 이 기록된다",
line.Model.Props.GetText("Orientation") == "Vertical");
// 굵기를 키우면 두께 축만 따라간다
line.Model.Props.SetText("BorderWidth", "3");
SheetMe.Core.Catalog.LineGeometry.NormalizeThickness(line.Model);
Check("선: 굵기는 두께 축만 바꾼다", Math.Abs(line.Model.Bounds.W - 3) < 0.5
&& Math.Abs(line.Model.Bounds.H - 200) < 0.5,
$"{line.Model.Bounds.W}×{line.Model.Bounds.H}");
// 가로로 되돌리면 Orientation 키가 지워진다(레거시 직렬화기가 기본값을 생략하는 것과 같다)
SheetMe.Core.Catalog.LineGeometry.ApplyOrientation(line.Model, vertical: false);
Check("선: 가로로 되돌리면 Orientation 키가 지워진다",
line.Model.Props.GetText("Orientation") is null
&& Math.Abs(line.Model.Bounds.W - 200) < 0.5
&& Math.Abs(line.Model.Bounds.H - 3) < 0.5,
$"{line.Model.Bounds.W}×{line.Model.Bounds.H}");
designer.Selection.SetSingle(line);
designer.DeleteSelection();
}
// 20-5f) 선택 밖 컨트롤을 Ctrl+드래그하면 그것이 끌려야 한다(기존 선택은 유지)
{
var anchor = Find("CheckBox");
var grabbed = Find("Label");
designer.Selection.SetSingle(anchor);
var anchorAt = new Point(anchor.X, anchor.Y);
var box = designer.WorldBoundsOf(grabbed);
var from = new Point(box.X + box.Width / 2, box.Y + box.Height / 2);
var grabbedX = grabbed.X;
// Alt 로 정렬 스냅을 끈다 — 스냅이 걸리면 이동량이 가이드에 붙어 델타 단언이 흔들린다
var ctrlAlt = new PointerContext(true, false, true, 1);
designer.Interaction.PointerDown(from, ctrlAlt);
designer.Interaction.PointerMove(new Point(from.X + 30, from.Y), ctrlAlt);
designer.Interaction.PointerUp(new Point(from.X + 30, from.Y), ctrlAlt);
Check("Ctrl+드래그: 잡은 컨트롤이 움직인다", Math.Abs(grabbed.X - (grabbedX + 30)) < 2,
$"{grabbedX} → {grabbed.X}");
Check("Ctrl+드래그: 기존 선택도 함께 움직인다(합집합)", Math.Abs(anchor.X - (anchorAt.X + 30)) < 2,
$"{anchorAt.X} → {anchor.X}");
designer.Undo.Undo();
designer.Selection.Clear();
}
// 20-5d) 태그 카탈로그 — 레거시에서 주석 처리된 태그를 고를 수 있으면 안 된다
Check("태그 카탈로그: 미구현 3종 제외",
!SheetMe.Core.Catalog.LegacyTagCatalog.DataInterfaceTags.Contains("PAT_BMI")
@@ -779,6 +844,22 @@ public static class EditSmoke
var addRow = designer.Inspector.Rows
.OfType<ViewModels.Inspector.AddPropertyRowViewModel>().FirstOrDefault();
Check("고급: 속성 추가 행 존재", addRow is not null);
// 자동완성 — 타입별 유효 키를 제안한다(철자를 외워 맞히지 않도록)
Check("고급: 제안 목록 존재", addRow!.Suggestions.Count > 0, $"{addRow.Suggestions.Count}개");
Check("고급: 이미 있는 키와 큐레이션된 키는 제안에서 빠진다",
!addRow.Suggestions.Contains("AutoHeight") && !addRow.Suggestions.Contains("Text"),
"AutoHeight/Text 는 TextBox 인스펙터가 이미 다룬다");
addRow.KeyText = "Print";
Check("고급: 부분 일치로 좁혀진다", addRow.Matches.Count > 0
&& addRow.Matches.All(m => m.Contains("Print", StringComparison.OrdinalIgnoreCase)),
string.Join(",", addRow.Matches));
Check("고급: 접두 일치가 앞에 온다",
addRow.Matches[0].StartsWith("Print", StringComparison.OrdinalIgnoreCase), addRow.Matches[0]);
addRow.KeyText = string.Empty;
// 라벨의 소문자 visible — 대문자로 쓰면 라벨이 그대로 보인다. 제안이 이 함정을 덮어야 한다.
var labelSuggest = SheetMe.Core.Catalog.LegacyPropertyCatalog.SuggestFor("Label", Array.Empty<string>());
Check("고급: 라벨 제안에는 대문자 Visible 이 없다",
!labelSuggest.Contains("Visible"), string.Join(",", labelSuggest.Where(s => s.Contains("isible"))));
addRow!.KeyText = "EnterTabYon";
addRow.AddCommand!.Execute(null, EventArgs.Empty);
Check("고급: 속성 추가됨", advTarget.Model.Props.Contains("EnterTabYon"));