다크/라이트 대비 전수 감사 및 수정 — 전환 미추종 3종 + 라이트 대비 미달 일괄

사용자 지적: "다크, 화이트 모드 오류나는(대비색상 제대로 표현안됨) 부분 모두 확인해줘".
표면별 병렬 감사 후 지적 74건을 하나씩 반증 검증해 39건 확정(21건 반증). 원인은 셋으로 압축된다.

━━ 1. 전환을 아예 따라가지 못하던 것(라이브 전환 시 옛 색 유지) ━━

[아이콘 브러시 굽기 — 가장 컸다] IconParam.Parse 가 TryFindResource 로 Brush 를 꺼내
LucideIcons.Icon 이 Path.Stroke/Fill 에 대입했다. ThemeManager 는 토큰 사전을 통째로 갈아끼우므로
이미 만들어진 아이콘은 옛 색을 영원히 유지한다. 실측: 라이트 전환 후 플로팅 바의 선택/손 도구가
다크의 B.Ink(#EDEDED)를 붙든 채 흰 배경에 놓여 사라졌다.
→ Parse 가 Brush 대신 **키**를 반환하고 Icon 이 SetResourceReference 로 건다(DynamicResource 등가).
   시작 시점에는 LoadSaved 가 MainView 생성보다 앞이라 정상이었고, 전환 시에만 나던 결함이다.

[그림자 3곳] 플로팅 바는 <StaticResource ResourceKey="B.FloatShadow"/> 요소 구문이라 굳었고,
두 플라이아웃은 아예 리터럴 검정(Opacity 0.4)이었다. 라이트 설계값은 0.14 인데 흰 배경에 진한
검정이 그대로 얹혔다. 셋 다 DynamicResource 토큰 참조로.

[콤보 드롭다운 그림자] 같은 이유로 리터럴 #000000/0.22 → 토큰.

━━ 2. 암시 스타일이 없어 WPF 기본 크롬으로 떨어지던 것(다크에서 안 보임) ━━

ListView·ListViewItem·GridViewColumnHeader·ToolBar·ToolBarTray 는 각자 DefaultStyleKey 를 자기
타입으로 재정의해 기존 ListBox 계열 암시 스타일을 물려받지 못한다. 스타일이 없으니 WPF 기본(Aero2)
흰 시스템 크롬으로 떨어지는데 그 안의 글자는 앱 전역 암시 TextBlock(B.Ink)을 받는다 → **다크에서
흰 바탕에 흰 글자**. 피해 화면: DB에서 서식 열기, 서식 수정이력, 미리보기 창 툴바.
→ 5종 암시 스타일 신설. 행 템플릿은 반드시 GridViewRowPresenter 를 쓴다 — ListBoxItem 을
   BasedOn 으로 물려주면 ContentPresenter 가 열 구성을 대체해 행이 바인딩 객체의 ToString()
   한 줄로 렌더된다(구현 중 실제로 겪었고 스크린샷으로 확인 후 고쳤다).

[캡션 글리프] CaptionBtn 템플릿이 TextElement.Foreground 상속으로 색을 넘기는데, Content 가
<TextBlock> 요소면 그 자신의 암시 스타일(B.Ink)이 상속을 이긴다. B.CaptionFg/FgHover/CloseHover
4개 토큰이 통째로 죽은 코드였다 → ContentPresenter 지역 암시 스타일로 되돌려 잡음.

━━ 3. 값 자체가 라이트에서 기준 미달(토큰 한 곳을 고치면 수십 곳이 낫는다) ━━

B.Muted #969696 → #6B6B6B (라이트만)
  양 테마 동일값이라 라이트 배경에서 2.50~2.96:1 — 본문 4.5:1 은 물론 아이콘 3:1 도 미달이었다
  (다크는 같은 값이 4.85~6.53:1). 회색 하나를 양 테마가 공유할 수 없다. 사용처 59곳이 전부
  Foreground 라 채움 룩에는 영향이 없다. 새 값은 Panel 5.33 / AppBg 4.89 / Canvas 4.51 / Chip 4.63.

B.CanvasBg #ECECEC → #D8D8D8   종이 대비 1.18 → 1.43:1 (다크는 14.35:1인데 라이트만 용지 경계가
                                사실상 없었다). 용지 자체는 손대지 않았다.
B.InputBorder #E3E3E3 → #C4C4C4  라이트는 Input=Panel=Surface=#FFFFFF 라 이 테두리가 입력란의
                                유일한 경계인데 1.28:1 이었다.
B.Line2 #D8D8D8 → #C6C6C6      체크박스 상자·컨트롤 경계
B.Sel #DBEEFF → #C2E2FF        선택 채움 1.19:1 → 어느 항목이 선택됐는지 안 보였다
B.Hover #F3F3F3 → #EAEAEA
B.Accent2 #E4F2FF → #CFE7FF    도구 '켜짐' 표시
B.Success #2E9E6B → #217A52    결과 문구 2.94:1 → 4.6:1
B.SliderRing #CFCFCF → #9A9A9A 흰 썸의 유일한 윤곽

신규 토큰 2종(브랜드색 #0099FF 는 그대로 두고 역할만 분리):
  B.AccentText — 강조 '글자'. 흰 배경 위 3.0:1 로는 본문이 안 된다 → 라이트 #0F6FBF(5.19), 다크는
                 어두운 배경이라 브랜드색 그대로(6.09). 쿼리 편집기·마스크 편집기 헤더에 적용.
  B.AccentFill — Primary 버튼 채움. 흰 글자를 얹으므로 양 테마 #0F6FBF(5.19). 브랜드색 위 흰 글자는
                 3.0:1 로 본문 미달이었다.

그 외: MiniToggle Opacity 0.55 → 0.75(옅은 색을 또 반으로 깎아 라이트 2.22:1 이었다 → 3.19/4.02),
스크롤 막대·화살표가 이미 있는 B.ScrollThumb* 토큰을 무시하고 리터럴을 쓰던 것 정리(라이트에서
3:1 미달이고 호버·드래그로 갈수록 더 흐려지는 어포던스 역전이었다), TextBox 포커스 표시를 호버와
같은 B.Line2 에서 B.Accent 로(포커스 위치가 유일한 단서인데 1.3:1 이었다), 메뉴 키보드 하이라이트
B.Hover(1.1:1) → B.Sel, 도구 토글 '켜짐'에 강조 테두리 추가(채움만으로는 1.14~1.19:1),
쿼리 편집기 GridSplitter 를 공용 ColSplitter 스타일로, 색상 피커 견본 테두리를 테마색이 아니라
견본 자신의 명도로 결정(라이트에서 흰 견본·다크에서 검정 견본이 빈칸으로 보였다).

━━ 판정을 바꾸지 않은 것 ━━
용지 렌더 도메인(ControlTemplates/PageView/PrintService/SelectionOverlay/인라인 편집기)의 고정색은
**정상**이다. 레거시 서식을 1:1로 그리는 곳이라 테마가 새어 들어가는 쪽이 버그다. 감사에서 이 경계를
반대로 지적한 건들은 반증 처리했다.
남는 한계: 컨트롤 경계의 WCAG 3:1 을 완전히 채우려면 라이트 테두리를 #949494 까지 올려야 하는데
[200] 의 가벼운 룩을 크게 벗어나 가시성 확보선(#C4C4C4)에서 멈췄다.

검증: 테스트 124/124, edit-smoke 실패 0, 왕복 1,271건 diff 0/예외 0,
**종이 렌더 P062 픽셀 대조 차이 0**(토큰을 여럿 바꿨는데도 용지에 새지 않음을 확인).
실 UI: 다크↔라이트 왕복 전환 후 아이콘 색 추종 확인, 라이트 전 화면·인스펙터·레이어,
DB에서 서식 열기 대화상자를 양 테마에서 열어 열 구성과 가독성 확인.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Msystech
2026-08-12 12:13:28 +09:00
co-authored by Claude Opus 5
parent ca16630f3a
commit ff87353d0d
9 changed files with 267 additions and 50 deletions
@@ -169,9 +169,12 @@ public sealed class ColorPickerWindow : Window
var swatch = new Button
{
Width = 22, Height = 22, Margin = new Thickness(0, 0, 4, 4), Padding = new Thickness(0),
ToolTip = hex, BorderBrush = FindBrush("B.Line2"),
ToolTip = hex,
Background = new SolidColorBrush(ParseHex(hex) ?? Colors.White),
};
// 견본 테두리는 테마색이 아니라 견본 자신의 명도에서 정한다 — 테마 테두리를 쓰면
// 라이트에서 흰 견본이, 다크에서 검정 견본이 배경에 묻혀 빈칸으로 보인다.
swatch.BorderBrush = ContrastEdge(ParseHex(hex) ?? Colors.White);
var captured = hex;
swatch.Click += (_, _) =>
{
@@ -191,6 +194,22 @@ public sealed class ColorPickerWindow : Window
private static Brush FindBrush(string key)
=> Application.Current.TryFindResource(key) as Brush ?? Brushes.Gray;
/// <summary>
/// 색 견본의 윤곽선 — 견본 자신의 밝기에 따라 어두운/밝은 테두리를 고른다.
/// 테마 토큰(B.Line2)을 쓰면 라이트에서 흰 견본이, 다크에서 검정 견본이 배경에 묻혀
/// 빈칸으로 보인다. 견본은 색 자체가 데이터라 배경이 아니라 견본 기준으로 판단해야 한다.
/// </summary>
private static Brush ContrastEdge(Color color)
{
// WCAG 상대휘도의 단순 근사 — 견본 테두리 판정에는 이 정도면 충분하다
var luminance = (0.2126 * color.R + 0.7152 * color.G + 0.0722 * color.B) / 255.0;
var brush = new SolidColorBrush(luminance > 0.6
? Color.FromRgb(0x8A, 0x8A, 0x8A)
: Color.FromRgb(0xD0, 0xD0, 0xD0));
brush.Freeze();
return brush;
}
// ── 상호작용 → 상태 ──
private void UpdateSvFrom(Point point)
{
+8 -4
View File
@@ -649,9 +649,11 @@
<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">
<!-- 그림자는 토큰으로 — 라이트에서 흰 배경에 검은 그림자가 그대로 얹히던 것을 테마별로 분리 -->
<!-- 그림자는 토큰으로 — 라이트에서 흰 배경에 검은 그림자가 그대로 얹히던 것을 테마별로 분리.
반드시 DynamicResource — 요소 구문 <StaticResource/> 로 넣으면 로드 시점 값으로 굳어
테마를 바꿔도 다크용 진한 그림자가 그대로 남는다. -->
<Border.Effect>
<StaticResource ResourceKey="B.FloatShadow"/>
<DynamicResource ResourceKey="B.FloatShadow"/>
</Border.Effect>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<ToggleButton Style="{StaticResource BarToolToggle}" Width="38" Height="34" Padding="0" Focusable="False"
@@ -687,8 +689,9 @@
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"/>
<DynamicResource ResourceKey="B.FloatShadow"/>
</Border.Effect>
<ScrollViewer MaxHeight="440" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
<ItemsControl ItemsSource="{Binding Source={StaticResource PaletteGrouped}}" Margin="10,9,10,10">
@@ -732,8 +735,9 @@
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"/>
<DynamicResource ResourceKey="B.FloatShadow"/>
</Border.Effect>
<StackPanel Margin="5">
<Button Style="{StaticResource FlyoutRow}" Command="{Binding ZoomInCommand}" Click="OnZoomRowClick">
@@ -38,7 +38,7 @@
<TextBlock Grid.Row="1" Grid.Column="0" Text="빈 값 표시" VerticalAlignment="Center" Margin="0,0,8,6"
Foreground="{DynamicResource B.Muted}"/>
<TextBlock x:Name="PromptPreview" Grid.Row="1" Grid.Column="1" VerticalAlignment="Center" Margin="0,0,0,6"
FontFamily="Consolas, D2Coding, 굴림체" Foreground="{DynamicResource B.Accent}"/>
FontFamily="Consolas, D2Coding, 굴림체" Foreground="{DynamicResource B.AccentText}"/>
<TextBlock Grid.Row="2" Grid.Column="0" Text="시험 입력" VerticalAlignment="Center" Margin="0,0,8,0"
Foreground="{DynamicResource B.Muted}"
@@ -81,7 +81,7 @@
<GroupStyle.HeaderTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" FontWeight="Bold" Margin="2,8,0,3"
Foreground="{DynamicResource B.Accent}"/>
Foreground="{DynamicResource B.AccentText}"/>
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
@@ -28,7 +28,8 @@
HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"
TextChanged="OnSqlChanged"/>
<GridSplitter Grid.Column="1" HorizontalAlignment="Stretch" Background="{DynamicResource B.Line}"/>
<!-- 공용 ColSplitter 스타일을 쓴다 — Background 만 주면 양 테마에서 배경과 1.1~1.3:1 로 붙어 손잡이가 안 보인다 -->
<GridSplitter Grid.Column="1" Style="{StaticResource ColSplitter}"/>
<!-- 치환 변수 목록 (SQL 용도에서만 표시) -->
<DockPanel x:Name="VariablePane" Grid.Column="2">
@@ -51,7 +52,7 @@
<DataTemplate>
<TextBlock Text="{Binding Name}" FontWeight="Bold" Margin="2,8,0,3"
FontFamily="Segoe UI, 맑은 고딕" FontSize="12"
Foreground="{DynamicResource B.Accent}"/>
Foreground="{DynamicResource B.AccentText}"/>
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>