diff --git a/src/SheetMe.Core/Catalog/LegacyQueryVariableCatalog.cs b/src/SheetMe.Core/Catalog/LegacyQueryVariableCatalog.cs
new file mode 100644
index 0000000..b518f52
--- /dev/null
+++ b/src/SheetMe.Core/Catalog/LegacyQueryVariableCatalog.cs
@@ -0,0 +1,66 @@
+namespace SheetMe.Core.Catalog;
+
+/// 치환 변수 1건 — 이 SQL 에 그대로 삽입되는 문자열이다.
+public sealed record QueryVariable(string Group, string Token, string Description);
+
+///
+/// MDataTable 쿼리의 치환 변수 카탈로그.
+///
+/// 레거시 런타임 bzDesignSheetLoader.ConvertQuery 는 토큰에서
+/// sReserved.Substring(0, sReserved.LastIndexOf(".")) 를 잘라
+/// PatientInfo/SheetInfo/WorkInfo 객체의 GetType.FullName 과
+/// Select Case 완전일치로 비교한다. 즉 접두어는 클래스 전체 이름이어야 하며,
+/// 축약형(<<PatientInfo.X>>)은 치환되지 않고 <<...>> 가
+/// SQL 에 리터럴로 남아 ORA 구문오류를 낸다(= 해당 데이터소스를 참조하는 컨트롤이 전부 공백).
+///
+/// 접두어는 런타임 호스트의 속성 선언형으로 고정된다
+/// (ucLoadSheetBase.vb:7619-7631 — PatientInfo/WorkInfo 는 M.CMM.HISOperatingInfo,
+/// SheetInfo 는 M.EMR.SheetLoadOperatingInfo).
+///
+/// 속성명은 리플렉션(GetProperty(...).GetValue(...))으로 해석되므로 실존하는 공개 스칼라
+/// 속성만 등재한다. DataRow 접근형(...PatInfDR.item("컬럼"))은 위 LastIndexOf(".") 규칙이
+/// 접두어를 깨뜨려 이 경로에서 반드시 실패하므로 목록에 넣지 않는다.
+///
+public static class LegacyQueryVariableCatalog
+{
+ #region Member Fields
+ private const string Patient = "M.CMM.HISOperatingInfo.bzPatientInfo";
+ private const string Sheet = "M.EMR.SheetLoadOperatingInfo.bzSheetInfo";
+ private const string Work = "M.CMM.HISOperatingInfo.bzWorkInfo";
+
+ /// 그룹별 치환 변수 — 인스펙터 쿼리 편집기의 삽입 목록
+ public static readonly IReadOnlyList All = new QueryVariable[]
+ {
+ new("환자", $"<<{Patient}.ChtNum>>", "차트번호"),
+ new("환자", $"<<{Patient}.ComNum>>", "내원번호"),
+ new("환자", $"<<{Patient}.ComNum_Refer>>", "참조 내원번호"),
+ new("환자", $"<<{Patient}.ComCvtCom>>", "전환 내원번호"),
+ new("환자", $"<<{Patient}.PatTyp>>", "입퇴원 구분"),
+ new("환자", $"<<{Patient}.AdpDtm>>", "접수일시"),
+ new("환자", $"<<{Patient}.OrderAdpDtm>>", "처방 접수일시"),
+ new("환자", $"<<{Patient}.Age>>", "나이"),
+ new("환자", $"<<{Patient}.AgeInMonth>>", "월령"),
+ new("환자", $"<<{Patient}.Sex>>", "성별"),
+ new("환자", $"<<{Patient}.PatMblPhn>>", "휴대전화"),
+ new("환자", $"<<{Patient}.ComDayCar>>", "낮병동 여부"),
+ new("환자", $"<<{Patient}.HipassYon>>", "하이패스 여부"),
+ new("환자", $"<<{Patient}.EmgKTSGrd>>", "응급 KTAS 등급"),
+ new("환자", $"<<{Patient}.OemNum1>>", "외부연동 번호1"),
+ new("환자", $"<<{Patient}.OemNum2>>", "외부연동 번호2"),
+ new("환자", $"<<{Patient}.OemVar1>>", "외부연동 값1"),
+
+ new("서식", $"<<{Sheet}.ShtCod>>", "서식 코드"),
+ new("서식", $"<<{Sheet}.ShtNam>>", "서식명"),
+ new("서식", $"<<{Sheet}.EmrKey>>", "EMR 키(신규 작성 시 0)"),
+ new("서식", $"<<{Sheet}.AdpDtm>>", "작성일시"),
+ new("서식", $"<<{Sheet}.TemKey>>", "템플릿 키"),
+
+ new("작업자", $"<<{Work}.WrkUid>>", "작성자 ID"),
+ new("작업자", $"<<{Work}.WrkDte>>", "작업일자(yyyyMMdd)"),
+ new("작업자", $"<<{Work}.WrkDtm>>", "작업일시"),
+ };
+
+ /// 런타임이 완전일치로 비교하는 접두어 3종 — 검증·진단용
+ public static readonly IReadOnlyList Prefixes = new[] { Patient, Sheet, Work };
+ #endregion
+}
diff --git a/src/SheetMe.Designer/Diagnostics/EditSmoke.cs b/src/SheetMe.Designer/Diagnostics/EditSmoke.cs
index c98959a..4b288f6 100644
--- a/src/SheetMe.Designer/Diagnostics/EditSmoke.cs
+++ b/src/SheetMe.Designer/Diagnostics/EditSmoke.cs
@@ -384,7 +384,24 @@ public static class EditSmoke
var fromJson = jsonSerializer.Read(json);
Check("JSON 왕복 — XML 동일", serializer.Write(fromJson) == serializer.Write(designer.Document));
- // 22) 연속 Undo 로 빈 문서까지
+ // 22) 쿼리 편집기 — XAML 로드/바인딩과 두 모드(SQL/상용구)를 실제로 생성해 확인
+ var sqlEditor = new Views.QueryEditorWindow("스모크", "SELECT 1 FROM DUAL");
+ var variableItems = (sqlEditor.FindName("VariableList") as System.Windows.Controls.ListBox)?
+ .ItemsSource?.Cast