초기 커밋: 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,110 @@
using System.Windows;
using System.Windows.Input;
using Microsoft.Xaml.Behaviors;
using SheetMe.Designer.Services;
using SheetMe.Designer.ViewModels;
namespace SheetMe.Designer.Behaviors;
/// <summary>
/// 캔버스 마우스 입력 → 월드 좌표 → InteractionController 위임.
/// World 요소(LayoutTransform 줌 하위)에 부착 — GetPosition(World)가 줌 불변 논리좌표를 반환.
/// 로직 없음: 좌표 변환·캡처·커서 매핑만 담당.
/// </summary>
public sealed class CanvasMouseBehavior : Behavior<FrameworkElement>
{
#region Methods
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.MouseLeftButtonDown += OnMouseDown;
AssociatedObject.MouseMove += OnMouseMove;
AssociatedObject.MouseLeftButtonUp += OnMouseUp;
AssociatedObject.LostMouseCapture += OnLostCapture;
}
protected override void OnDetaching()
{
AssociatedObject.MouseLeftButtonDown -= OnMouseDown;
AssociatedObject.MouseMove -= OnMouseMove;
AssociatedObject.MouseLeftButtonUp -= OnMouseUp;
AssociatedObject.LostMouseCapture -= OnLostCapture;
base.OnDetaching();
}
private DesignerViewModel? Designer => AssociatedObject.DataContext as DesignerViewModel;
private static PointerContext ContextOf(int clickCount = 1) => new(
Keyboard.Modifiers.HasFlag(ModifierKeys.Control),
Keyboard.Modifiers.HasFlag(ModifierKeys.Shift),
Keyboard.Modifiers.HasFlag(ModifierKeys.Alt),
clickCount);
private void OnMouseDown(object sender, MouseButtonEventArgs e)
{
if (Designer is not { } designer)
{
return;
}
// 키보드 포커스를 캔버스 컨테이너(Focusable 상위)로 — 단축키 수신
FocusCanvasHost();
designer.Interaction.PointerDown(e.GetPosition(AssociatedObject), ContextOf(e.ClickCount));
AssociatedObject.CaptureMouse();
e.Handled = true;
}
private void OnMouseMove(object sender, MouseEventArgs e)
{
if (Designer is not { } designer)
{
return;
}
var world = e.GetPosition(AssociatedObject);
if (e.LeftButton == MouseButtonState.Pressed && AssociatedObject.IsMouseCaptured)
{
designer.Interaction.PointerMove(world, ContextOf());
}
AssociatedObject.Cursor = MapCursor(designer.Interaction.CursorAt(world));
}
private void OnMouseUp(object sender, MouseButtonEventArgs e)
{
if (Designer is not { } designer)
{
return;
}
designer.Interaction.PointerUp(e.GetPosition(AssociatedObject), ContextOf());
AssociatedObject.ReleaseMouseCapture();
e.Handled = true;
}
private void OnLostCapture(object sender, MouseEventArgs e)
=> Designer?.Interaction.CancelDrag(); // 정상 업 이후에는 no-op — 선택 유지
private void FocusCanvasHost()
{
// 상위 DesignerCanvasView(Focusable)로 포커스 — 키보드 단축키 수신
DependencyObject? current = AssociatedObject;
while (current is not null)
{
if (current is FrameworkElement { Focusable: true } focusable)
{
focusable.Focus();
return;
}
current = System.Windows.Media.VisualTreeHelper.GetParent(current);
}
}
private static Cursor MapCursor(CursorKind kind) => kind switch
{
CursorKind.SizeAll => Cursors.SizeAll,
CursorKind.SizeNWSE => Cursors.SizeNWSE,
CursorKind.SizeNESW => Cursors.SizeNESW,
CursorKind.SizeNS => Cursors.SizeNS,
CursorKind.SizeWE => Cursors.SizeWE,
_ => Cursors.Arrow,
};
#endregion
}