초기 커밋: SheetMe 서식생성기 (P0~P5 완료 상태)

레거시 서식생성기(VB.NET WinForms) 대체용 C#/.NET 10 WPF 디자이너.
기준선: 실DB 활성 디자인 1,271건 왕복 의미론 diff 0 / 예외 0, 단위 테스트 49/49.

이 커밋에 함께 포함된 자격증명 분리:
- appsettings.json 을 __HOST__/__PASSWORD__ 플레이스홀더로 전환
- 실접속 정보는 appsettings.Development.json 으로 분리(.gitignore 제외,
  csproj Debug 조건부 복사라 Release 산출물에 실리지 않음)
- ConfigLoader 를 환경변수 > Development > appsettings 순 레이어링으로 변경,
  미치환 플레이스홀더는 '미설정'으로 간주

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Msystech
2026-08-11 17:20:22 +09:00
co-authored by Claude Fable 5
commit 16c07f48dc
102 changed files with 14210 additions and 0 deletions
@@ -0,0 +1,302 @@
using System.Windows.Media;
using SheetMe.Designer.ViewModels.Controls;
namespace SheetMe.Designer.ViewModels.Inspector;
/// <summary>
/// 인스펙터 행 ViewModel — 라벨 + 문자열 정규화 값. 커밋 시(값 변경 시에만) 소유자 콜백으로
/// Undo 스냅샷 → 전체 선택 대상 적용 → 시각 재해석이 수행된다.
/// </summary>
public abstract class PropertyRowViewModel : ViewModelBase
{
#region Member Fields
private string valueText = string.Empty;
private bool building;
#endregion
#region Properties
/// <summary>행 라벨(한글)</summary>
public string Label { get; }
/// <summary>선택 대상들의 값이 서로 다른지 — "여러 값" 표시</summary>
public bool IsMixed { get; private set; }
/// <summary>정규화 문자열 값 — 파생 편집기가 형 변환</summary>
public string ValueText
{
get => valueText;
set
{
if (!SetProperty(ref valueText, value) || building)
{
return;
}
IsMixed = false;
OnPropertyChanged(nameof(IsMixed));
Commit?.Invoke(value);
OnValueApplied();
}
}
/// <summary>커밋 콜백 — InspectorViewModel 이 배선(스냅샷+적용)</summary>
public Action<string>? Commit { get; set; }
#endregion
#region Constructors
protected PropertyRowViewModel(string label)
{
Label = label;
}
#endregion
#region Methods
/// <summary>초기값 세팅(커밋 미발생)</summary>
public void Initialize(string? value, bool isMixed = false)
{
building = true;
valueText = value ?? string.Empty;
IsMixed = isMixed;
OnPropertyChanged(nameof(ValueText));
OnPropertyChanged(nameof(IsMixed));
building = false;
}
/// <summary>값 적용 후 파생 갱신 지점</summary>
protected virtual void OnValueApplied() { }
#endregion
}
/// <summary>구분 헤더 행</summary>
public sealed class SectionRowViewModel : PropertyRowViewModel
{
public SectionRowViewModel(string label) : base(label) { }
}
/// <summary>한 줄 문자열 행</summary>
public sealed class TextRowViewModel : PropertyRowViewModel
{
public TextRowViewModel(string label) : base(label) { }
}
/// <summary>여러 줄 문자열 행(Text/수식/항목 목록)</summary>
public sealed class MultilineTextRowViewModel : PropertyRowViewModel
{
public MultilineTextRowViewModel(string label) : base(label) { }
}
/// <summary>숫자 행 — 문자열 바인딩, 커밋 시 숫자 검증은 소유자에서</summary>
public sealed class NumberRowViewModel : PropertyRowViewModel
{
public NumberRowViewModel(string label) : base(label) { }
}
/// <summary>토글(참/거짓) 행</summary>
public sealed class ToggleRowViewModel : PropertyRowViewModel
{
/// <summary>체크 상태 — "True"/"False" 문자열과 동기</summary>
public bool IsOn
{
get => ValueText == "True";
set => ValueText = value ? "True" : "False";
}
public ToggleRowViewModel(string label) : base(label) { }
protected override void OnValueApplied() => OnPropertyChanged(nameof(IsOn));
}
/// <summary>선택지 행</summary>
public sealed class ChoiceRowViewModel : PropertyRowViewModel
{
/// <summary>선택지 목록</summary>
public string[] Choices { get; }
public ChoiceRowViewModel(string label, string[] choices) : base(label)
{
Choices = choices;
}
}
/// <summary>
/// 태그 피커 행 — 값 표시 + 찾아보기 버튼(검색 대화상자).
/// 목록에 없는 사이트 커스텀 태그는 텍스트 직접 입력도 허용.
/// </summary>
public sealed class TagPickerRowViewModel : PropertyRowViewModel
{
/// <summary>피커 선택지(레거시 카탈로그)</summary>
public IReadOnlyList<string> Choices { get; }
/// <summary>피커 대화상자 제목</summary>
public string PickerTitle { get; }
/// <summary>찾아보기 — 검색 대화상자 열기</summary>
public M.Framework.WPF.ICustomCommand? BrowseCommand { get; set; }
public TagPickerRowViewModel(string label, string pickerTitle, IReadOnlyList<string> choices) : base(label)
{
PickerTitle = pickerTitle;
Choices = choices;
BrowseCommand = new M.Framework.WPF.Command((sender, e) => OnBrowse());
}
private void OnBrowse()
{
var dialog = new Views.TagPickerDialogView(PickerTitle, Choices, ValueText)
{
Owner = System.Windows.Application.Current.MainWindow,
};
if (dialog.ShowDialog() == true)
{
ValueText = dialog.SelectedTag ?? string.Empty;
}
}
}
/// <summary>SQL 쿼리 행 — 요약 표시 + 전용 편집기 창(치환 변수 삽입)</summary>
public sealed class QueryRowViewModel : PropertyRowViewModel
{
/// <summary>요약 텍스트(한 줄)</summary>
public string Summary
{
get
{
var oneLine = ValueText.Replace("\r", " ").Replace("\n", " ").Trim();
return oneLine.Length == 0 ? "(쿼리 없음)" : oneLine.Length > 48 ? oneLine[..48] + "…" : oneLine;
}
}
/// <summary>전용 편집기 열기</summary>
public M.Framework.WPF.ICustomCommand? EditCommand { get; set; }
public QueryRowViewModel(string label) : base(label)
{
EditCommand = new M.Framework.WPF.Command((sender, e) => OnEdit());
}
protected override void OnValueApplied() => OnPropertyChanged(nameof(Summary));
private void OnEdit()
{
var dialog = new Views.QueryEditorWindow(Label, ValueText)
{
Owner = System.Windows.Application.Current.MainWindow,
};
if (dialog.ShowDialog() == true)
{
ValueText = dialog.QueryText;
}
}
}
/// <summary>색 행 — 레거시 invariant 문자열("R, G, B"/명명색) + 미리보기 스와치</summary>
public sealed class ColorRowViewModel : PropertyRowViewModel
{
/// <summary>미리보기 브러시</summary>
public Brush Preview
{
get
{
if (ValueText.Length == 0)
{
return Brushes.Transparent;
}
var (a, r, g, b) = Core.Serialization.LegacyFormat.ParseColor(ValueText);
var brush = new SolidColorBrush(Color.FromArgb(a, r, g, b));
brush.Freeze();
return brush;
}
}
/// <summary>색상 피커 열기 — 확정 시 레거시 invariant 형식("R, G, B")으로 반영</summary>
public M.Framework.WPF.ICustomCommand? PickCommand { get; set; }
public ColorRowViewModel(string label) : base(label)
{
PickCommand = new M.Framework.WPF.Command((sender, e) => OnPick());
}
protected override void OnValueApplied() => OnPropertyChanged(nameof(Preview));
private void OnPick()
{
var initialHex = (string?)null;
if (ValueText.Length > 0)
{
var (_, r, g, b) = Core.Serialization.LegacyFormat.ParseColor(ValueText);
initialHex = $"#{r:X2}{g:X2}{b:X2}";
}
var picked = Views.ColorPickerWindow.Pick(System.Windows.Application.Current.MainWindow, initialHex);
if (picked is null)
{
return;
}
var color = (Color)ColorConverter.ConvertFromString(picked);
ValueText = Core.Serialization.LegacyFormat.FormatColor(255, color.R, color.G, color.B);
}
}
/// <summary>읽기 전용 행 — 중첩/바이너리/참조 등 raw 편집 불가 값 표시</summary>
public sealed class ReadOnlyRowViewModel : PropertyRowViewModel
{
public ReadOnlyRowViewModel(string label, string display) : base(label)
{
Initialize(display);
}
}
/// <summary>전체 속성(고급) 섹션 토글 행 — 표시/숨김 버튼</summary>
public sealed class ToggleAdvancedRowViewModel : PropertyRowViewModel
{
/// <summary>버튼 표시 텍스트</summary>
public string ButtonText { get; }
/// <summary>토글 실행</summary>
public M.Framework.WPF.ICustomCommand? ToggleCommand { get; set; }
public ToggleAdvancedRowViewModel(string buttonText, Action toggle) : base(string.Empty)
{
ButtonText = buttonText;
ToggleCommand = new M.Framework.WPF.Command((sender, e) => toggle());
}
}
/// <summary>속성 추가 행 — 키 입력 후 빈 속성 생성(고급)</summary>
public sealed class AddPropertyRowViewModel : PropertyRowViewModel
{
private string keyText = string.Empty;
/// <summary>추가할 속성 키(레거시 Property 이름)</summary>
public string KeyText
{
get => keyText;
set => SetProperty(ref keyText, value);
}
/// <summary>추가 실행</summary>
public M.Framework.WPF.ICustomCommand? AddCommand { get; set; }
public AddPropertyRowViewModel(Action<string> add) : base(string.Empty)
{
AddCommand = new M.Framework.WPF.Command((sender, e) =>
{
var key = KeyText.Trim();
if (key.Length > 0)
{
add(key);
}
});
}
}
/// <summary>인스펙터 행 컨텍스트 — 대상 컨트롤 집합과 접근자</summary>
public sealed class RowBinding
{
/// <summary>값 읽기</summary>
public required Func<ControlViewModel, string?> Get { get; init; }
/// <summary>값 쓰기</summary>
public required Action<ControlViewModel, string> Set { get; init; }
/// <summary>커밋 후 시각 재해석 필요 여부</summary>
public bool AffectsVisual { get; init; } = true;
}