라디오버튼도 패널로 묶는다 — 차단을 경고로 (운영 표준 패턴을 막고 있었다)
조사 워크플로(읽기 3 + 반박 검증 3)로 확정한 사실:
- 레거시 RadioButton 은 System.Windows.Forms.RadioButton 을 그대로 상속하고
GroupName 류 속성이 소스·IL 어디에도 없다 → <b>직계 부모가 곧 배타 단위</b>
(검증자가 실제 어셈블리를 로드해 Panel/GroupBox/MExpandablePanel/호스트가
각각 독립 집합이고, 패널 밖으로 옮기면 배타가 즉시 끊김을 실행으로 확인)
- 운영 전수(디자인 1,270건 전량 덤프): 라디오 15,184개 중 <b>15,026개(98.96%)가
패널 직계 자식</b>, 그룹 5,582개(평균 2.72 · 중앙값 2 · 최대 72),
최상위 직속은 158개(1.04%)뿐. 그룹을 나타내는 속성은 0건 —
즉 "패널 1개 = 문항 1개" 가 사실상의 저장 규약이다
그런데 우리는 라디오 Ctrl+G 를 <b>아예 막아</b> 두었다(GroupBlockReason ①) —
운영 서식의 98.96% 패턴을 우리 도구로는 만들 수 없었다는 뜻이다.
막은 이유("일부만 묶으면 택1이 갈라진다")는 타당하지만 그것은 경고할 일이지
기능을 없앨 일이 아니다. 오히려 막으면 한 페이지의 라디오가 전부 한 그룹이 되어
문항을 여러 개 만들 수 없다.
- 차단 제거, 대신 같은 자리에 <b>묶이지 않고 남는 라디오가 있을 때만</b>
"N개 중 M개만 묶습니다 — 남는 것은 다른 선택 묶음이 됩니다" 로 되묻는다
(RadioSplitWarning). 전부 묶으면 되묻지 않는다.
- edit-smoke: "라디오는 묶이지 않는다" 판정을 뒤집어 "라디오도 패널로 묶인다"
+ "패널 자식이 된다" + 경고 판정 2건(일부만/전부)으로 교체
- GroupSelection 의 낡은 XML 주석("동일 GroupId 부여")을 실제 동작(패널 래핑)과
근거 수치로 교체 — GroupId 계열은 이미 죽은 코드다
- dotnet test 358/358 · --edit-smoke 실패 0 · --db-patient ①~㊲ 전건 통과
- --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
345b2e1da1
commit
d965b3f217
@@ -1070,16 +1070,36 @@ public static class EditSmoke
|
||||
&& grp.Pages[0].Controls.Any(c => c.X == 240 && c.Y == 140),
|
||||
string.Join(" ", grp.Pages[0].Controls.Select(c => $"({c.X},{c.Y})")));
|
||||
|
||||
// 가드 ① 라디오는 부모가 곧 배타 단위 — 일부만 묶으면 '택1'이 갈라진다
|
||||
// 라디오는 <b>패널이 곧 문항</b>이다 — 묶는 것이 정상이고 필요한 작업이다.
|
||||
// 운영 실측: 라디오 9,935개 중 9,819개(98.8%)가 패널 자식, 그룹 3,625개
|
||||
// (평균 2.74개 · "패널 1개 = 질문 1개"가 사실상의 저장 규약).
|
||||
// 전에는 이 경로를 아예 막아 두어 <b>운영 서식의 표준 패턴을 만들 수 없었다</b>.
|
||||
var radioDoc = new DesignerViewModel(business.CreateNew());
|
||||
radioDoc.AddControlAt("RadioButton", new Point(100, 100));
|
||||
radioDoc.AddControlAt("RadioButton", new Point(100, 140));
|
||||
radioDoc.Selection.Set(radioDoc.Pages[0].Controls.ToList(), radioDoc.Pages[0].Controls[0]);
|
||||
var beforeRadio = radioDoc.Pages[0].Controls.Count;
|
||||
radioDoc.GroupSelection();
|
||||
Check("그룹 가드: 라디오는 묶이지 않는다",
|
||||
radioDoc.Pages[0].Controls.Count == beforeRadio
|
||||
&& !radioDoc.Pages[0].Controls.Any(c => c.Type == "Panel"));
|
||||
var radioPanel = radioDoc.Pages[0].Controls.OfType<ContainerViewModel>().FirstOrDefault(c => c.Type == "Panel");
|
||||
Check("그룹: 라디오도 패널로 묶인다", radioPanel is not null);
|
||||
Check("그룹: 묶인 라디오가 패널 자식이 된다",
|
||||
radioPanel is not null && radioPanel.Children.Count == 2
|
||||
&& radioPanel.Children.All(c => c.Type == "RadioButton"),
|
||||
radioPanel is null ? "패널 없음" : $"자식 {radioPanel.Children.Count}개");
|
||||
// 되묻는 경로(일부만 묶기)는 대화상자가 떠서 진단에서 끝까지 돌릴 수 없으므로
|
||||
// 경고 <b>판정 자체</b>를 직접 부른다 — 이게 문항이 갈라지는 유일한 방어선이다.
|
||||
var splitDoc = new DesignerViewModel(business.CreateNew());
|
||||
for (var i = 0; i < 3; i++)
|
||||
{
|
||||
splitDoc.AddControlAt("RadioButton", new Point(100, 100 + (i * 40)));
|
||||
}
|
||||
var splitPage = splitDoc.Pages[0];
|
||||
var twoOfThree = splitPage.Controls.Take(2).ToList();
|
||||
Check("그룹 경고: 라디오 일부만 묶으면 갈라진다고 말한다",
|
||||
splitDoc.RadioSplitWarning(splitPage, twoOfThree) is { } w
|
||||
&& w.Contains("3개 중 2개", StringComparison.Ordinal),
|
||||
splitDoc.RadioSplitWarning(splitPage, twoOfThree) ?? "경고 없음");
|
||||
Check("그룹 경고(대조군): 전부 묶으면 되묻지 않는다",
|
||||
splitDoc.RadioSplitWarning(splitPage, splitPage.Controls.ToList()) is null);
|
||||
|
||||
// 가드 ② 사이에 남이 끼어 있으면 쌓임 순서가 바뀐다
|
||||
var zDoc = new DesignerViewModel(business.CreateNew());
|
||||
|
||||
@@ -1769,8 +1769,13 @@ public sealed class DesignerViewModel : ViewModelBase
|
||||
=> Selection.Items.Count > 0 && Selection.Items.Select(PageOf).Distinct().Count() <= 1;
|
||||
|
||||
/// <summary>
|
||||
/// 선택 그룹화 — 동일 GroupId 부여(2개 이상, 최상위 컨트롤만) + 기본 이름 등록.
|
||||
/// 선택 그룹화 — 선택분(2개 이상, 최상위만)을 감싸는 <b>진짜 Panel</b> 을 만들어 자식으로 옮긴다.
|
||||
/// 그래서 저장까지 살아남는다(레거시 XML 의 children="Controls" 중첩 Object).
|
||||
/// 잠긴 컨트롤은 애초에 선택에 들어오지 않는다(SelectionService 가 거른다).
|
||||
///
|
||||
/// <b>라디오에 특히 중요하다</b>: 레거시는 GroupName 이 없어 직계 부모가 곧 배타 단위이고,
|
||||
/// 운영 실측도 라디오 15,184개 중 15,026개(98.96%)가 패널 자식이다(그룹 5,582개, 평균 2.72개).
|
||||
/// 즉 이 조작이 "문항 만들기" 다.
|
||||
/// </summary>
|
||||
public void GroupSelection()
|
||||
{
|
||||
@@ -1784,6 +1789,15 @@ public sealed class DesignerViewModel : ViewModelBase
|
||||
DialogService.Notify(DialogKind.Warning, "그룹", reason);
|
||||
return;
|
||||
}
|
||||
// 라디오는 <b>패널이 곧 문항</b>이다 — 묶는 것이 정상이고 필요한 작업이다
|
||||
// (운영 실측: 라디오 9,935개 중 9,819개(98.8%)가 패널 자식, 그룹 3,625개).
|
||||
// 다만 한 문항의 일부만 묶으면 '하나만 고르기'가 둘로 갈라지므로 그때만 되묻는다.
|
||||
if (RadioSplitWarning(page, targets) is { } warning
|
||||
&& !DialogService.Confirm("그룹", warning,
|
||||
"묶은 라디오끼리 하나만 선택되고, 남은 라디오는 별도 묶음이 됩니다."))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// z 순서상 멤버가 차지한 구간 — 패널은 그 자리에 들어간다.
|
||||
// WinForms 에서 형제는 남의 자식 사이에 낄 수 없어 멤버가 반드시 연속 블록이 되는데,
|
||||
@@ -1837,15 +1851,7 @@ public sealed class DesignerViewModel : ViewModelBase
|
||||
/// </summary>
|
||||
private string? GroupBlockReason(PageViewModel page, IReadOnlyList<ControlViewModel> targets)
|
||||
{
|
||||
// ① 라디오는 부모가 곧 배타 단위다(레거시 RadioButton 에 GroupName 이 없다).
|
||||
// '택1' 문항에서 일부만 묶으면 그 일부가 독립 집합이 되어 EMR 에서 답이 둘 선택된다.
|
||||
if (targets.Any(vm => vm.Type == "RadioButton"))
|
||||
{
|
||||
return "라디오 버튼은 패널이 곧 선택 묶음이라, 일부만 묶으면 '하나만 고르기'가 갈라집니다.\n"
|
||||
+ "라디오는 그룹으로 묶지 마세요.";
|
||||
}
|
||||
|
||||
// ② 멤버 사이에 남이 끼어 있으면 쌓임 순서가 바뀐다.
|
||||
// 멤버 사이에 남이 끼어 있으면 쌓임 순서가 바뀐다.
|
||||
var indices = targets.Select(vm => page.Model.Controls.IndexOf(vm.Model)).ToList();
|
||||
if (indices.Any(i => i < 0))
|
||||
{
|
||||
@@ -1860,6 +1866,32 @@ public sealed class DesignerViewModel : ViewModelBase
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 라디오 문항이 갈라지는가 — 갈라지지 않으면 null.
|
||||
///
|
||||
/// <b>왜 되묻는가.</b> 레거시 RadioButton 은 WinForms 를 그대로 상속해
|
||||
/// <c>GroupName</c> 이 없고 <b>직계 부모가 곧 배타 단위</b>다(RadioButton.vb:7-8).
|
||||
/// 그래서 패널로 묶는 것 자체는 정상이고 필요한 작업이지만,
|
||||
/// 같은 자리에 있던 라디오 중 <b>일부만</b> 묶으면 한 문항이 두 묶음으로 갈라져
|
||||
/// EMR 에서 답이 둘 선택된다 — 캔버스에는 아무 변화도 안 보인 채로.
|
||||
/// </summary>
|
||||
internal string? RadioSplitWarning(PageViewModel page, IReadOnlyList<ControlViewModel> targets)
|
||||
{
|
||||
var picked = targets.Count(vm => vm.Type == "RadioButton");
|
||||
if (picked == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
// 지금은 최상위만 묶을 수 있으므로(위 targets 필터) 남는 형제도 최상위에서 센다
|
||||
var siblings = page.Model.Controls.Count(c =>
|
||||
string.Equals(c.Type, "RadioButton", StringComparison.Ordinal)
|
||||
&& !targets.Any(vm => ReferenceEquals(vm.Model, c)));
|
||||
return siblings == 0
|
||||
? null
|
||||
: $"이 페이지에 라디오 버튼 {picked + siblings}개 중 {picked}개만 묶습니다.\n"
|
||||
+ $"남는 {siblings}개는 다른 선택 묶음이 됩니다. 계속할까요?";
|
||||
}
|
||||
|
||||
private PageViewModel? PageOf2(int index) => index >= 0 && index < Pages.Count ? Pages[index] : null;
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user