choiminyeong 5e5ef8c36f 플랫폼 최초 커밋 + 자동 업데이트 배포 폴더(dist)
소스(Core/Shell/Launcher)와 실행 PC가 받아갈 업데이트 파일(dist)을 함께 올린다.
Launcher가 켤 때 이 저장소의 dist와 D:\UbisamPlatform\bin을 비교해 새 버전이면 받아간다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 21:56:35 +09:00

8.9 KiB

패치 03 — 하단 탭 모양 정리 + 선택 인디케이터 슬라이드 애니메이션

첨부하신 첫 번째 사진(아이콘 위 / 라벨 아래 / 라벨 아래 밑줄만) 기준으로 맞추고, 탭을 전환하면 그 밑줄이 선택한 탭으로 미끄러져 이동합니다. 화면 컨텐츠는 건드리지 않고 하단 탭 바에만 애니메이션이 들어갑니다.

이 패치는 패치 01·02를 포함해 대체합니다 — ShellStyles.xaml의 하단 탭 관련 두 스타일을 통째로 교체합니다.

1. 모양 (사진 1 기준)

  • 칸 사이 세로 구분선 제거
  • 선택 시 상단 3px 바 + 옅은 accent 판 → 바닥 3px 밑줄만
  • 선택 배경 틴트 제거, 선택된 탭은 글자·아이콘 색만 accent
  • 마우스 오버는 선택되지 않은 탭에만 옅은 틴트
  • 탭은 가운데 모임(균등 분할 아님), 아이템 패딩 30,13,30,14

2. src/UbisamBase.Core/Themes/ShellStyles.xaml

Ubisam.Style.SidebarItem.Horizontal과 Ubisam.Style.SidebarBar.Horizontal을 아래 두 스타일로 교체하세요. SidebarBar.Horizontal에는 ControlTemplate이 새로 들어갑니다 — 그 안의 SelectionIndicator / SelectionIndicatorShift 이름을 코드비하인드가 찾습니다. 이름을 바꾸지 마세요.

<Style x:Key="Ubisam.Style.SidebarItem.Horizontal" TargetType="ListBoxItem">
    <Setter Property="Padding" Value="30,13,30,14"/>
    <Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.Sidebar.Foreground}"/>
    <Setter Property="FontFamily" Value="{StaticResource Ubisam.Font.Body}"/>
    <Setter Property="FontSize" Value="13.5"/>
    <Setter Property="FontWeight" Value="Medium"/>
    <Setter Property="Cursor" Value="Hand"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="ListBoxItem">
                <Border x:Name="Bd" Background="Transparent" Padding="{TemplateBinding Padding}">
                    <StackPanel Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center">
                        <Path x:Name="Ico"
                              Data="{Binding Icon, Converter={StaticResource IconToGeometry}}"
                              Stroke="{Binding Foreground, RelativeSource={RelativeSource TemplatedParent}}"
                              StrokeThickness="1.5"
                              StrokeStartLineCap="Round" StrokeEndLineCap="Round" StrokeLineJoin="Round"
                              Width="22" Height="22" Stretch="Uniform"
                              Margin="0,0,0,7" HorizontalAlignment="Center"/>
                        <TextBlock Text="{Binding Title}" HorizontalAlignment="Center" TextTrimming="CharacterEllipsis"/>
                    </StackPanel>
                </Border>
                <ControlTemplate.Triggers>
                    <DataTrigger Binding="{Binding Icon, Converter={StaticResource IconToGeometry}}" Value="{x:Null}">
                        <Setter TargetName="Ico" Property="Visibility" Value="Collapsed"/>
                    </DataTrigger>
                    <Trigger Property="IsSelected" Value="True">
                        <Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.Sidebar.SelectedForeground}"/>
                    </Trigger>
                    <MultiTrigger>
                        <MultiTrigger.Conditions>
                            <Condition Property="IsMouseOver" Value="True"/>
                            <Condition Property="IsSelected" Value="False"/>
                        </MultiTrigger.Conditions>
                        <Setter TargetName="Bd" Property="Background" Value="{DynamicResource Ubisam.Brush.Sidebar.HoverBackground}"/>
                    </MultiTrigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

<Style x:Key="Ubisam.Style.SidebarBar.Horizontal" TargetType="ListBox">
    <Setter Property="Background" Value="{DynamicResource Ubisam.Brush.Sidebar.Background}"/>
    <Setter Property="BorderThickness" Value="0,1,0,0"/>
    <Setter Property="BorderBrush" Value="{DynamicResource Ubisam.Brush.SubTabBar.Border}"/>
    <Setter Property="Padding" Value="0"/>
    <Setter Property="ItemContainerStyle" Value="{StaticResource Ubisam.Style.SidebarItem.Horizontal}"/>
    <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled"/>
    <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Disabled"/>
    <Setter Property="ItemsPanel">
        <Setter.Value>
            <ItemsPanelTemplate>
                <StackPanel Orientation="Horizontal" HorizontalAlignment="Center"/>
            </ItemsPanelTemplate>
        </Setter.Value>
    </Setter>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="ListBox">
                <Border Background="{TemplateBinding Background}"
                        BorderBrush="{TemplateBinding BorderBrush}"
                        BorderThickness="{TemplateBinding BorderThickness}">
                    <Grid>
                        <ItemsPresenter/>
                        <Border x:Name="SelectionIndicator"
                                Height="3" Width="0"
                                HorizontalAlignment="Left" VerticalAlignment="Bottom"
                                Background="{DynamicResource Ubisam.Brush.Accent}">
                            <Border.RenderTransform>
                                <TranslateTransform x:Name="SelectionIndicatorShift" X="0"/>
                            </Border.RenderTransform>
                        </Border>
                    </Grid>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

3. src/UbisamBase.Core/Shell/ShellWindow.xaml.cs — 애니메이션

using 추가:

using System.Windows.Media.Animation;

ShellWindow 생성자 마지막에 이벤트 두 개를 붙이세요.

NavList.SelectionChanged += (_, _) => MoveTabIndicator(animate: true);
NavList.Loaded += (_, _) => MoveTabIndicator(animate: false);
NavList.SizeChanged += (_, _) => MoveTabIndicator(animate: false);

그리고 메서드를 추가하세요.

/// <summary>하단 탭의 선택 인디케이터를 선택된 탭 아래로 옮긴다. 좌/우 네비게이션일 때는 인디케이터가 없어 무시된다.</summary>
private void MoveTabIndicator(bool animate)
{
    if (NavList.Template?.FindName("SelectionIndicator", NavList) is not Border indicator ||
        NavList.Template.FindName("SelectionIndicatorShift", NavList) is not TranslateTransform shift)
    {
        return;
    }

    if (NavList.SelectedItem == null ||
        NavList.ItemContainerGenerator.ContainerFromItem(NavList.SelectedItem) is not ListBoxItem container ||
        !container.IsArrangeValid)
    {
        indicator.Width = 0;
        return;
    }

    var origin = container.TranslatePoint(new Point(0, 0), NavList);
    var targetX = origin.X;
    var targetWidth = container.ActualWidth;

    if (!animate)
    {
        shift.BeginAnimation(TranslateTransform.XProperty, null);
        indicator.BeginAnimation(FrameworkElement.WidthProperty, null);
        shift.X = targetX;
        indicator.Width = targetWidth;
        return;
    }

    var duration = new Duration(TimeSpan.FromMilliseconds(260));
    var ease = new CubicEase { EasingMode = EasingMode.EaseOut };

    shift.BeginAnimation(TranslateTransform.XProperty,
        new DoubleAnimation(targetX, duration) { EasingFunction = ease, FillBehavior = FillBehavior.HoldEnd });
    indicator.BeginAnimation(FrameworkElement.WidthProperty,
        new DoubleAnimation(targetWidth, duration) { EasingFunction = ease, FillBehavior = FillBehavior.HoldEnd });
}

ApplyNavPosition에서 위치를 바꾼 뒤에도 한 번 호출해 주세요 (좌/우 → 하단 전환 시 위치 재계산).

NavList.Width = isHorizontal ? double.NaN : 140;
NavList.Height = double.NaN;

if (isHorizontal)
{
    Dispatcher.BeginInvoke(new Action(() => MoveTabIndicator(animate: false)));
}

4. 확인

  1. 탭을 누르면 밑줄이 260ms에 걸쳐 미끄러져 오고, 너비도 새 탭 폭에 맞게 함께 변하는지
  2. 앱을 처음 켰을 때는 애니메이션 없이 첫 탭 아래에 바로 놓이는지
  3. 창 크기를 바꾸거나 최대화할 때 밑줄이 어긋나지 않는지
  4. 설정에서 네비게이션 위치를 좌/우로 바꿨다가 하단으로 되돌렸을 때 밑줄이 정상 위치인지
  5. 권한에 따라 탭이 필터링(navView.Refresh())된 뒤에도 밑줄이 맞는지 — 어긋나면 OnAuthChanged 끝에 MoveTabIndicator(animate: false)를 한 줄 추가하세요

참고

  • 260ms가 느리면 TimeSpan.FromMilliseconds(160)으로 줄이세요.
  • 밑줄을 라벨 폭만큼만 짧게 하고 싶으면 targetWidth를 container.ActualWidth - 40 정도로 줄이고 targetX에 20을 더하면 됩니다.