초기 커밋: 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
+540
View File
@@ -0,0 +1,540 @@
<Window x:Class="SheetMe.Designer.Views.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:shell="clr-namespace:System.Windows.Shell;assembly=PresentationFramework"
xmlns:vm="clr-namespace:SheetMe.Designer.ViewModels"
xmlns:v="clr-namespace:SheetMe.Designer.Views"
xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
xmlns:bh="clr-namespace:SheetMe.Designer.Behaviors"
xmlns:ctl="clr-namespace:SheetMe.Designer.Controls"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance vm:MainViewModel}"
Title="{Binding Title}"
Icon="pack://application:,,,/SheetMe.Designer;component/Assets/sheetme.ico"
Width="1440" Height="920" MinWidth="940" MinHeight="640"
WindowStyle="None" WindowStartupLocation="CenterScreen"
Background="{DynamicResource B.AppBg}" FontFamily="Malgun Gothic" FontSize="13"
Loaded="OnWindowLoaded" StateChanged="OnWindowStateChanged">
<!-- GlassFrameThickness 0,0,0,1: DWM 창 그림자 활성화([200]SheetMe 크롬과 동일) -->
<shell:WindowChrome.WindowChrome>
<shell:WindowChrome CaptionHeight="40" ResizeBorderThickness="6" GlassFrameThickness="0,0,0,1"
CornerRadius="0" UseAeroCaptionButtons="False"/>
</shell:WindowChrome.WindowChrome>
<Window.DataContext>
<vm:MainViewModel/>
</Window.DataContext>
<Window.Resources>
<ctl:TypeToIconConverter x:Key="TypeToIcon"/>
<ctl:TypeToCategoryConverter x:Key="TypeToCategory"/>
<ctl:IconNameToVisualConverter x:Key="IconName"/>
<ctl:InverseBoolConverter x:Key="InverseBool"/>
<!-- 팔레트(도구 상자/플라이아웃 공용) — 카테고리 그룹 뷰 -->
<CollectionViewSource x:Key="PaletteGrouped" Source="{Binding PaletteItems}">
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="Type" Converter="{StaticResource TypeToCategory}"/>
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
<!-- 도구 상자 타일 카드([200] 컴포넌트 카드 룩) — 선택 하이라이트 없음 -->
<Style x:Key="PaletteTile" TargetType="ListBoxItem">
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Border x:Name="bd" Background="{DynamicResource B.Surface}" BorderBrush="{DynamicResource B.Line}"
BorderThickness="1" CornerRadius="5" Margin="3" SnapsToDevicePixels="True">
<ContentPresenter/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="bd" Property="Background" Value="{DynamicResource B.Hover}"/>
<Setter TargetName="bd" Property="BorderBrush" Value="{DynamicResource B.Line2}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- 팔레트 카테고리 그룹 헤더 -->
<Style x:Key="PaletteGroupHeader" TargetType="TextBlock">
<Setter Property="FontSize" Value="10.5"/>
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="Foreground" Value="{DynamicResource B.Muted}"/>
<Setter Property="Margin" Value="5,9,0,4"/>
</Style>
<!-- 플라이아웃/줌 팝업 행 버튼 — 왼정렬 hover 행 -->
<Style x:Key="FlyoutRow" TargetType="Button">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="{DynamicResource B.Ink}"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Padding" Value="12,7"/>
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="bd" Background="{TemplateBinding Background}" CornerRadius="6"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="bd" Property="Background" Value="{DynamicResource B.Hover}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<DockPanel>
<!-- ===== 커스텀 타이틀바: 로고 + 메뉴 | 중앙 제목 | 창 제어 ===== -->
<Border DockPanel.Dock="Top" Background="{DynamicResource B.Titlebar}" Height="40"
BorderBrush="{DynamicResource B.Line}" BorderThickness="0,0,0,1">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- 좌: 로고 + 메뉴 -->
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center" Margin="10,0,0,0"
shell:WindowChrome.IsHitTestVisibleInChrome="True">
<Image Source="pack://application:,,,/SheetMe.Designer;component/Assets/sheetme-logo.png"
Width="20" Height="20" VerticalAlignment="Center" RenderOptions.BitmapScalingMode="HighQuality"/>
<Menu VerticalAlignment="Center" Margin="8,0,0,0">
<MenuItem Header="파일(_F)">
<MenuItem Header="새 서식(_N)" Command="{Binding NewFileCommand}" InputGestureText="Ctrl+N"/>
<MenuItem Header="열기(_O)..." Command="{Binding OpenFileCommand}" InputGestureText="Ctrl+O"/>
<Separator/>
<MenuItem Header="저장(_S)" Command="{Binding SaveFileCommand}" InputGestureText="Ctrl+S"/>
<MenuItem Header="다른 이름으로 저장(_A)..." Command="{Binding SaveAsFileCommand}"/>
<Separator/>
<MenuItem Header="DB에서 열기(_D)..." Command="{Binding OpenFromDbCommand}"/>
<MenuItem Header="DB에 저장(_B)..." Command="{Binding SaveToDbCommand}"/>
<Separator/>
<MenuItem Header="JSON 가져오기(_I)..." Command="{Binding ImportJsonCommand}"/>
<MenuItem Header="JSON 내보내기(_E)..." Command="{Binding ExportJsonCommand}"/>
<Separator/>
<MenuItem Header="미리보기(_V)..." Command="{Binding PreviewCommand}"/>
<MenuItem Header="인쇄(_P)..." Command="{Binding PrintCommand}" InputGestureText="Ctrl+P"/>
<Separator/>
<MenuItem Header="종료(_X)" Command="{Binding ExitCommand}"/>
</MenuItem>
<MenuItem Header="편집(_E)">
<MenuItem Header="실행 취소(_U)" Command="{Binding CurrentDesigner.UndoCommand}" InputGestureText="Ctrl+Z"/>
<MenuItem Header="다시 실행(_R)" Command="{Binding CurrentDesigner.RedoCommand}" InputGestureText="Ctrl+Y"/>
<Separator/>
<MenuItem Header="잘라내기(_T)" Command="{Binding CurrentDesigner.CutCommand}" InputGestureText="Ctrl+X"/>
<MenuItem Header="복사(_C)" Command="{Binding CurrentDesigner.CopyCommand}" InputGestureText="Ctrl+C"/>
<MenuItem Header="붙여넣기(_P)" Command="{Binding CurrentDesigner.PasteCommand}" InputGestureText="Ctrl+V"/>
<MenuItem Header="복제(_D)" Command="{Binding CurrentDesigner.DuplicateCommand}" InputGestureText="Ctrl+D"/>
<MenuItem Header="삭제(_L)" Command="{Binding CurrentDesigner.DeleteCommand}" InputGestureText="Del"/>
<Separator/>
<MenuItem Header="전체 선택(_A)" Command="{Binding CurrentDesigner.SelectAllCommand}" InputGestureText="Ctrl+A"/>
</MenuItem>
<MenuItem Header="배치(_A)">
<MenuItem Header="맨 앞으로(_F)" Command="{Binding CurrentDesigner.BringToFrontCommand}"/>
<MenuItem Header="맨 뒤로(_B)" Command="{Binding CurrentDesigner.SendToBackCommand}"/>
<Separator/>
<MenuItem Header="그룹(_G)" Command="{Binding CurrentDesigner.GroupCommand}" InputGestureText="Ctrl+G"/>
<MenuItem Header="그룹 해제(_U)" Command="{Binding CurrentDesigner.UngroupCommand}" InputGestureText="Ctrl+Shift+G"/>
<Separator/>
<MenuItem Header="왼쪽 맞춤" Command="{Binding CurrentDesigner.AlignCommand}" CommandParameter="Left"/>
<MenuItem Header="오른쪽 맞춤" Command="{Binding CurrentDesigner.AlignCommand}" CommandParameter="Right"/>
<MenuItem Header="위 맞춤" Command="{Binding CurrentDesigner.AlignCommand}" CommandParameter="Top"/>
<MenuItem Header="아래 맞춤" Command="{Binding CurrentDesigner.AlignCommand}" CommandParameter="Bottom"/>
<MenuItem Header="가로 가운데" Command="{Binding CurrentDesigner.AlignCommand}" CommandParameter="CenterH"/>
<MenuItem Header="세로 가운데" Command="{Binding CurrentDesigner.AlignCommand}" CommandParameter="CenterV"/>
<Separator/>
<MenuItem Header="같은 크기로">
<MenuItem Header="너비/높이 모두(_B)" Command="{Binding CurrentDesigner.SizeToControlCommand}" CommandParameter="Both"/>
<MenuItem Header="너비(_W)" Command="{Binding CurrentDesigner.SizeToControlCommand}" CommandParameter="Width"/>
<MenuItem Header="높이(_H)" Command="{Binding CurrentDesigner.SizeToControlCommand}" CommandParameter="Height"/>
</MenuItem>
<MenuItem Header="가로 간격">
<MenuItem Header="균등(_E)" Command="{Binding CurrentDesigner.SpacingCommand}" CommandParameter="HorizEqual"/>
<MenuItem Header="넓게(_I)" Command="{Binding CurrentDesigner.SpacingCommand}" CommandParameter="HorizIncrease"/>
<MenuItem Header="좁게(_D)" Command="{Binding CurrentDesigner.SpacingCommand}" CommandParameter="HorizDecrease"/>
<MenuItem Header="붙이기(_C)" Command="{Binding CurrentDesigner.SpacingCommand}" CommandParameter="HorizConcat"/>
</MenuItem>
<MenuItem Header="세로 간격">
<MenuItem Header="균등(_E)" Command="{Binding CurrentDesigner.SpacingCommand}" CommandParameter="VertEqual"/>
<MenuItem Header="넓게(_I)" Command="{Binding CurrentDesigner.SpacingCommand}" CommandParameter="VertIncrease"/>
<MenuItem Header="좁게(_D)" Command="{Binding CurrentDesigner.SpacingCommand}" CommandParameter="VertDecrease"/>
<MenuItem Header="붙이기(_C)" Command="{Binding CurrentDesigner.SpacingCommand}" CommandParameter="VertConcat"/>
</MenuItem>
<Separator/>
<MenuItem Header="페이지 가로 가운데(_H)" Command="{Binding CurrentDesigner.CenterInPageCommand}" CommandParameter="H"/>
<MenuItem Header="페이지 세로 가운데(_V)" Command="{Binding CurrentDesigner.CenterInPageCommand}" CommandParameter="V"/>
<Separator/>
<MenuItem Header="{Binding CurrentDesigner.TabOrderMenuHeader, FallbackValue=탭순서 편집 시작}"
Command="{Binding CurrentDesigner.TabOrderCommand}"
IsChecked="{Binding CurrentDesigner.IsTabOrderMode, Mode=OneWay}"
ToolTip="입력 컨트롤을 원하는 입력 순서대로 클릭한 뒤 완료 — Esc 취소"/>
</MenuItem>
<MenuItem Header="보기(_V)">
<MenuItem Header="확대(_I)" Command="{Binding ZoomInCommand}" InputGestureText="Ctrl+휠↑"/>
<MenuItem Header="축소(_O)" Command="{Binding ZoomOutCommand}" InputGestureText="Ctrl+휠↓"/>
<MenuItem Header="100%(_R)" Command="{Binding ZoomResetCommand}"/>
<Separator/>
<MenuItem x:Name="ThemeMenuItem" Header="다크 테마(_D)" IsCheckable="True" Click="OnThemeToggleClick"/>
</MenuItem>
<MenuItem Header="도구(_T)">
<MenuItem Header="서식 수정이력(_H)..." Command="{Binding SheetHistoryCommand}"/>
<Separator/>
<MenuItem Header="상용구 관리(_W)..." Command="{Binding RecordWordCommand}"/>
<MenuItem Header="폰트 일괄 변경(_F)..." Command="{Binding FontManagerCommand}"/>
</MenuItem>
</Menu>
</StackPanel>
<!-- 중앙: 창 제목 -->
<TextBlock Grid.Column="1" Text="{Binding Title}" Foreground="{DynamicResource B.Muted}"
HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="12"/>
<!-- 우: 창 제어 -->
<StackPanel Grid.Column="2" Orientation="Horizontal" shell:WindowChrome.IsHitTestVisibleInChrome="True">
<Button Style="{StaticResource CaptionBtn}" Click="OnMinimizeClick" ToolTip="최소화">
<TextBlock Text="&#xE921;" FontFamily="Segoe MDL2 Assets" FontSize="10"/>
</Button>
<Button Style="{StaticResource CaptionBtn}" Click="OnMaxRestoreClick" ToolTip="최대화/복원">
<TextBlock x:Name="MaxGlyph" Text="&#xE922;" FontFamily="Segoe MDL2 Assets" FontSize="10"/>
</Button>
<Button Style="{StaticResource CloseBtn}" Click="OnCloseWinClick" ToolTip="닫기">
<TextBlock Text="&#xE8BB;" FontFamily="Segoe MDL2 Assets" FontSize="10"/>
</Button>
</StackPanel>
</Grid>
</Border>
<!-- ===== 하단 상태바 (줌은 플로팅 바로 이동) ===== -->
<Border DockPanel.Dock="Bottom" Background="{DynamicResource B.Titlebar}"
BorderBrush="{DynamicResource B.Line}" BorderThickness="0,1,0,0" Padding="12,6">
<TextBlock Text="{Binding StatusText}" Foreground="{DynamicResource B.Muted}" FontSize="12"
TextTrimming="CharacterEllipsis"/>
</Border>
<!-- ===== 본문 3열: 좌 탭 패널 | 문서 탭+캔버스+플로팅 바 | 속성 ([200] 레이아웃) ===== -->
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="232" MinWidth="180" MaxWidth="480"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="280" MinWidth="220" MaxWidth="560"/>
</Grid.ColumnDefinitions>
<!-- 좌: 알약 탭 — 서식 목록 / 레이어 / 도구 상자 -->
<Border Grid.Column="0" Background="{DynamicResource B.Panel}">
<TabControl x:Name="LeftTabs" SelectedIndex="{Binding SelectedLeftTabIndex, Mode=TwoWay}">
<!-- 서식 목록 -->
<TabItem>
<TabItem.Header><TextBlock Text="서식 목록" Style="{StaticResource TabHeaderText}"/></TabItem.Header>
<DockPanel>
<DockPanel DockPanel.Dock="Top" Margin="8,8,8,4">
<Button DockPanel.Dock="Right" Content="검색" Margin="6,0,0,0"
Command="{Binding SearchSheetsCommand}"/>
<TextBox Text="{Binding SheetSearchKeyword, UpdateSourceTrigger=PropertyChanged}"
ToolTip="서식명/코드 검색 (Enter)">
<TextBox.InputBindings>
<KeyBinding Key="Enter" Command="{Binding SearchSheetsCommand}"/>
</TextBox.InputBindings>
</TextBox>
</DockPanel>
<Grid>
<ListBox x:Name="SheetListBox" ItemsSource="{Binding SheetList}"
Margin="4,0,4,4"
VirtualizingPanel.IsVirtualizing="True"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
MouseDoubleClick="OnSheetListDoubleClick"
ToolTip="더블클릭으로 열기 — 여러 서식을 탭으로 동시에 편집할 수 있습니다">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Margin="1">
<TextBlock Text="{Binding ShtCod}" FontSize="11" Foreground="{DynamicResource B.Muted}"
Width="52" VerticalAlignment="Center"/>
<TextBlock Text="{Binding Name}" FontSize="12" TextTrimming="CharacterEllipsis">
<TextBlock.Style>
<Style TargetType="TextBlock" BasedOn="{StaticResource {x:Type TextBlock}}">
<Style.Triggers>
<DataTrigger Binding="{Binding HasDesign}" Value="False">
<Setter Property="Foreground" Value="{DynamicResource B.Muted}"/>
<Setter Property="Opacity" Value="0.6"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<TextBlock Text="불러오는 중..." HorizontalAlignment="Center" VerticalAlignment="Center"
Foreground="{DynamicResource B.Muted}"
Visibility="{Binding IsSheetListLoading, Converter={StaticResource BoolToVisibility}}"/>
</Grid>
</DockPanel>
</TabItem>
<!-- 레이어 (+ 페이지) -->
<TabItem>
<TabItem.Header><TextBlock Text="레이어" Style="{StaticResource TabHeaderText}"/></TabItem.Header>
<DockPanel>
<!-- 페이지 섹션 -->
<DockPanel DockPanel.Dock="Top" Margin="0,4,0,0">
<TextBlock DockPanel.Dock="Top" Text="페이지" FontWeight="Bold" FontSize="11"
Margin="10,4,10,4" Foreground="{DynamicResource B.Muted}"/>
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" Margin="10,2,10,6">
<Button Content="추가" Command="{Binding CurrentDesigner.AddPageCommand}"/>
<Button Content="삭제" Margin="6,0,0,0" Command="{Binding CurrentDesigner.RemovePageCommand}"/>
</StackPanel>
<ListBox ItemsSource="{Binding CurrentDesigner.Pages}"
SelectedItem="{Binding CurrentDesigner.SelectedPage}"
MaxHeight="110" Margin="4,0">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Margin="2,1">
<TextBlock Text="{Binding Index, StringFormat=페이지 {0}}" FontSize="12"/>
<TextBlock Text="{Binding SizeText, StringFormat= ({0})}" FontSize="11" Foreground="{DynamicResource B.Muted}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
<Border DockPanel.Dock="Top" Height="1" Background="{DynamicResource B.Line}" Margin="8,2"/>
<!-- 레이어 목록 (활성 페이지, 그리기 순서) -->
<ListBox ItemsSource="{Binding CurrentDesigner.SelectedPage.Controls}"
SelectedItem="{Binding CurrentDesigner.SelectedLayerItem}"
Margin="4,2,4,6"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemTemplate>
<DataTemplate>
<DockPanel Margin="0,1">
<CheckBox DockPanel.Dock="Right" IsChecked="{Binding IsLockedFlag}"
ToolTip="잠금" Margin="4,0,0,0"/>
<CheckBox DockPanel.Dock="Right" IsChecked="{Binding IsHiddenFlag}"
ToolTip="숨김" Margin="4,0,0,0"/>
<TextBlock Text="{Binding Id}" FontSize="12" TextTrimming="CharacterEllipsis"/>
</DockPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
</TabItem>
<!-- 도구 상자 ([200] 타일 카드 룩 — 드래그/더블클릭 배치) -->
<TabItem>
<TabItem.Header><TextBlock Text="도구 상자" Style="{StaticResource TabHeaderText}"/></TabItem.Header>
<ListBox ItemsSource="{Binding Source={StaticResource PaletteGrouped}}"
ItemContainerStyle="{StaticResource PaletteTile}"
Margin="6,4,6,6"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ToolTip="더블클릭 또는 드래그로 캔버스에 배치">
<b:Interaction.Behaviors>
<bh:PaletteDragBehavior/>
</b:Interaction.Behaviors>
<!-- 그룹 세로 나열(GroupStyle.Panel 기본) + 그룹 내 타일 2열(ItemsPanel — 그룹 내부 항목이 상속) -->
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="2"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" Style="{StaticResource PaletteGroupHeader}"/>
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ListBox.GroupStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel HorizontalAlignment="Center" Margin="2,9,2,8" Background="Transparent">
<ContentControl Content="{Binding Type, Converter={StaticResource TypeToIcon}}"
HorizontalAlignment="Center" Margin="0,0,0,5" IsTabStop="False" Focusable="False"/>
<TextBlock Text="{Binding DisplayName}" FontSize="11" TextAlignment="Center"
HorizontalAlignment="Center" TextWrapping="Wrap"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</TabItem>
</TabControl>
</Border>
<GridSplitter Grid.Column="1" Style="{StaticResource ColSplitter}" ToolTip="좌측 패널 너비 조절"/>
<!-- 중앙: 문서 탭(크롬식) + 캔버스 + 플로팅 팔레트 바 -->
<Grid Grid.Column="2" Background="{DynamicResource B.CanvasBg}">
<TabControl Style="{StaticResource DocTabs}"
ItemsSource="{Binding OpenDesigners}"
SelectedItem="{Binding CurrentDesigner}"
ItemContainerStyle="{StaticResource DocTabItem}">
<TabControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<ContentControl Content="{Binding Source=file-text, Converter={StaticResource IconName}, ConverterParameter=12}"
Margin="0,0,6,0" VerticalAlignment="Center" IsTabStop="False" Focusable="False"/>
<TextBlock Text="{Binding DisplayName}" MaxWidth="170" Style="{StaticResource TabHeaderText}"
TextTrimming="CharacterEllipsis" VerticalAlignment="Center"/>
<Button Content="✕" FontSize="9" Margin="7,0,0,0"
Style="{StaticResource Subtle}" Padding="3,0" Cursor="Hand" ToolTip="문서 닫기"
Command="{Binding DataContext.CloseDocumentCommand,
RelativeSource={RelativeSource AncestorType=Window}}"
CommandParameter="{Binding}"/>
</StackPanel>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<v:DesignerCanvasView/>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
<!-- 플로팅 팔레트 바 (Figma 식 — 컨트롤 추가 플라이아웃 | 줌) -->
<Border Background="{DynamicResource B.Surface}" BorderBrush="{DynamicResource B.Line}" BorderThickness="1"
CornerRadius="12" Padding="7,5" HorizontalAlignment="Center" VerticalAlignment="Bottom"
Margin="0,0,0,20">
<Border.Effect>
<DropShadowEffect BlurRadius="20" ShadowDepth="3" Opacity="0.35" Color="Black"/>
</Border.Effect>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<ToggleButton Width="38" Height="34" Padding="0" Focusable="False"
ToolTip="선택 도구 — 클릭으로 선택, 드래그로 이동"
IsChecked="{Binding IsHandTool, Converter={StaticResource InverseBool}, Mode=OneWay}"
Click="OnSelectToolClick"
Content="{Binding Source=mouse-pointer-click, Converter={StaticResource IconName}, ConverterParameter=18}"/>
<ToggleButton Width="38" Height="34" Padding="0" Focusable="False"
ToolTip="손 도구 — 드래그로 화면 이동 (Space 누르는 동안 임시)"
IsChecked="{Binding IsHandTool, Mode=OneWay}"
Click="OnHandToolClick"
Content="{Binding Source=hand, Converter={StaticResource IconName}, ConverterParameter=18}"/>
<Border Width="1" Height="22" Background="{DynamicResource B.Line}" Margin="5,0" VerticalAlignment="Center"/>
<Button x:Name="AddControlBtn" Style="{StaticResource Subtle}" Width="38" Height="34" Padding="0"
ToolTip="컨트롤 추가 — 활성 페이지 중앙에 배치" Click="OnAddControlFlyoutClick"
Content="{Binding Source=layout-grid, Converter={StaticResource IconName}, ConverterParameter=18}"/>
<Border Width="1" Height="22" Background="{DynamicResource B.Line}" Margin="5,0" VerticalAlignment="Center"/>
<Button Style="{StaticResource Subtle}" Width="30" Height="34" Padding="0" ToolTip="축소 (Ctrl+휠↓)"
Command="{Binding ZoomOutCommand}"
Content="{Binding Source=zoom-out, Converter={StaticResource IconName}, ConverterParameter=16}"/>
<Button x:Name="ZoomBtn" Style="{StaticResource Subtle}" Height="34" Padding="8,0,6,0" ToolTip="줌"
Click="OnZoomFlyoutClick">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="{Binding CurrentDesigner.ZoomPercentText, FallbackValue=100%}"
FontSize="12" MinWidth="38" TextAlignment="Center" VerticalAlignment="Center"/>
<ContentControl Content="{Binding Source=chevron-down, Converter={StaticResource IconName}, ConverterParameter=11}"
Margin="3,1,0,0" VerticalAlignment="Center" IsTabStop="False" Focusable="False"/>
</StackPanel>
</Button>
<Button Style="{StaticResource Subtle}" Width="30" Height="34" Padding="0" ToolTip="확대 (Ctrl+휠↑)"
Command="{Binding ZoomInCommand}"
Content="{Binding Source=zoom-in, Converter={StaticResource IconName}, ConverterParameter=16}"/>
</StackPanel>
</Border>
<!-- 컨트롤 추가 플라이아웃 (카테고리별 타일) -->
<Popup x:Name="AddControlPopup" PlacementTarget="{Binding ElementName=AddControlBtn}" Placement="Top"
StaysOpen="False" AllowsTransparency="True" PopupAnimation="Fade" VerticalOffset="-8">
<Border Margin="18" Background="{DynamicResource B.Panel}" BorderBrush="{DynamicResource B.Line}"
BorderThickness="1" CornerRadius="10" MinWidth="300">
<Border.Effect>
<DropShadowEffect BlurRadius="16" ShadowDepth="3" Opacity="0.4" Color="Black"/>
</Border.Effect>
<ScrollViewer MaxHeight="440" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
<ItemsControl ItemsSource="{Binding Source={StaticResource PaletteGrouped}}" Margin="10,9,10,10">
<!-- 그룹 세로 나열 + 그룹 내 타일 4열([200] 플라이아웃 치수) -->
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="4"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" FontSize="10.5" FontWeight="Bold"
Foreground="{DynamicResource B.Muted}" Margin="2,9,0,6"/>
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ItemsControl.GroupStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Width="66" Margin="3" Padding="4,9,4,8" Click="OnFlyoutTileClick"
Background="{DynamicResource B.Surface}" BorderBrush="{DynamicResource B.Line}"
ToolTip="{Binding DisplayName}">
<StackPanel HorizontalAlignment="Center">
<ContentControl Content="{Binding Type, Converter={StaticResource TypeToIcon}}"
HorizontalAlignment="Center" Margin="0,0,0,5" IsTabStop="False" Focusable="False"/>
<TextBlock Text="{Binding DisplayName}" FontSize="11" TextAlignment="Center"
HorizontalAlignment="Center"/>
</StackPanel>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Border>
</Popup>
<!-- 줌 플라이아웃 -->
<Popup x:Name="ZoomPopup" PlacementTarget="{Binding ElementName=ZoomBtn}" Placement="Top"
StaysOpen="False" AllowsTransparency="True" PopupAnimation="Fade" VerticalOffset="-8">
<Border Margin="18" Background="{DynamicResource B.Panel}" BorderBrush="{DynamicResource B.Line}"
BorderThickness="1" CornerRadius="10" MinWidth="190">
<Border.Effect>
<DropShadowEffect BlurRadius="16" ShadowDepth="3" Opacity="0.4" Color="Black"/>
</Border.Effect>
<StackPanel Margin="5">
<Button Style="{StaticResource FlyoutRow}" Command="{Binding ZoomInCommand}" Click="OnZoomRowClick">
<DockPanel LastChildFill="True">
<TextBlock DockPanel.Dock="Right" Text="Ctrl+휠↑" Foreground="{DynamicResource B.Muted}"
FontSize="11.5" Margin="24,0,0,0" VerticalAlignment="Center"/>
<TextBlock Text="확대" FontSize="12.5" VerticalAlignment="Center"/>
</DockPanel>
</Button>
<Button Style="{StaticResource FlyoutRow}" Command="{Binding ZoomOutCommand}" Click="OnZoomRowClick">
<DockPanel LastChildFill="True">
<TextBlock DockPanel.Dock="Right" Text="Ctrl+휠↓" Foreground="{DynamicResource B.Muted}"
FontSize="11.5" Margin="24,0,0,0" VerticalAlignment="Center"/>
<TextBlock Text="축소" FontSize="12.5" VerticalAlignment="Center"/>
</DockPanel>
</Button>
<Border Height="1" Background="{DynamicResource B.Line}" Margin="8,5"/>
<Button Style="{StaticResource FlyoutRow}" Command="{Binding ZoomResetCommand}" Click="OnZoomRowClick">
<TextBlock Text="100%" FontSize="12.5" VerticalAlignment="Center"/>
</Button>
</StackPanel>
</Border>
</Popup>
</Grid>
<GridSplitter Grid.Column="3" Style="{StaticResource ColSplitter}" ToolTip="속성 패널 너비 조절"/>
<!-- 우: 속성 인스펙터 -->
<Border Grid.Column="4" Background="{DynamicResource B.Panel}">
<DockPanel>
<Border DockPanel.Dock="Top" Background="{DynamicResource B.PanelHeader}" Padding="11,8"
BorderBrush="{DynamicResource B.Line}" BorderThickness="0,0,0,1">
<TextBlock Text="속성" FontWeight="Bold" Foreground="{DynamicResource B.Muted}"/>
</Border>
<v:InspectorView DataContext="{Binding CurrentDesigner.Inspector}"/>
</DockPanel>
</Border>
</Grid>
</DockPanel>
</Window>