데스크톱 T3 잔여 완성 + 스타일 폴리시: 도메인 라우팅(WorkspaceRoutingRules 자동 워크스페이스 전환), 탭 아카이브(TabArchive 영속·팔레트 복구), 페이지 액션(흑백/세피아 필터), 노트 패널(NotesStore+플라이아웃), 컴팩트 모드(크롬 숨김+엣지 호버). 스타일: 한국어 폰트 폴백, 암시 폰트 통일, 다크 툴팁, 텍스트 대비 상향. UpdateNavState dispose 가드. 164/164 GREEN + 실창 스크린샷 검증

This commit is contained in:
Yun Chan 2026-08-16 08:46:54 +09:00
parent 04ff0ff93d
commit f380518919
12 changed files with 594 additions and 24 deletions

View file

@ -50,6 +50,15 @@ public partial class MainWindow : Window
// Phase C: 팔레트/부스트/워크스페이스/레이아웃
private readonly CommandPalette _palette = new();
private BoostStore _boostStore = null!;
private WorkspaceRoutingRules _routingRules = null!; // T3-1
private TabArchive _tabArchive = null!; // T3-2
internal TabArchive Archive => _tabArchive; // E2E 검증용
internal BoostStore BoostStore => _boostStore; // E2E 검증용
internal NotesStore Notes => _notes; // E2E 검증용
private NotesStore _notes = null!; // T3-12
private string? _currentWorkspaceName; // T3-1 라우팅 기준
private bool _compactMode; // T3-13
private bool _chromeRevealed; // T3-13 호버 복원 상태
private List<WorkspaceSnapshot> _workspaces = new();
private bool _verticalTabs;
private TabItem? _splitTab;
@ -101,6 +110,9 @@ public partial class MainWindow : Window
_resolver = new SearchEngineResolver(_config.SearchEngine);
_zoomStore = new SiteZoomStore(Path.Combine(AppDataDir, "sitezoom.json"));
_boostStore = new BoostStore(Path.Combine(AppDataDir, "boost.json"));
_routingRules = new WorkspaceRoutingRules(Path.Combine(AppDataDir, "routing.json")); // T3-1
_tabArchive = new TabArchive(Path.Combine(AppDataDir, "archive.json")); // T3-2
_notes = new NotesStore(Path.Combine(AppDataDir, "notes.json")); // T3-12
try
{
var wsPath = Path.Combine(AppDataDir, "workspaces.json");
@ -434,16 +446,22 @@ public partial class MainWindow : Window
private void OnTabRemoved(TabItem tab)
{
if (ReferenceEquals(_splitTab, tab)) ExitSplit();
_tabArchive.Add(tab.Title, tab.Url); // T3-2 닫힘 → 아카이브(영속, 500 상한)
_views?.RemoveView(tab);
}
private void UpdateNavState()
{
var cv = Cv;
Dispatcher.BeginInvoke(new Action(() =>
{
NavBack.IsEnabled = cv?.CanGoBack == true;
NavForward.IsEnabled = cv?.CanGoForward == true;
try
{
var cv = Cv; // dispose 이후 이벤트 경로 방어
NavBack.IsEnabled = cv?.CanGoBack == true;
NavForward.IsEnabled = cv?.CanGoForward == true;
}
catch (ObjectDisposedException) { }
catch (InvalidOperationException) { }
}));
}
@ -717,6 +735,16 @@ public partial class MainWindow : Window
// T1-4: URL/검색어 판별 + 기본 엔진·키워드 전환(g, yt, ...)은 리졸버가 담당
var intent = _resolver.Resolve(url);
if (intent == null) return;
// T3-1: 도메인 라우팅 규칙 — 대상 워크스페이스가 다르면 자동 전환 후 이동(Arc ATC 패턴)
var routeTo = _routingRules.Match(intent.Target);
if (routeTo != null && routeTo != _currentWorkspaceName &&
_workspaces.FirstOrDefault(w => w.Name == routeTo) is { } wsTarget)
{
SwitchWorkspace(wsTarget);
_currentWorkspaceName = routeTo;
Log($"라우팅 규칙 → 워크스페이스 '{routeTo}' 전환");
}
Cv?.Navigate(intent.Target);
}
@ -778,6 +806,8 @@ public partial class MainWindow : Window
else if (mods == (ModifierKeys.Control | ModifierKeys.Shift) && e.Key == Key.D) { ToggleBoostDark(); e.Handled = true; }
else if (mods == (ModifierKeys.Control | ModifierKeys.Shift) && e.Key == Key.S) { _ = TakeScreenshot(); e.Handled = true; }
else if (mods == (ModifierKeys.Control | ModifierKeys.Shift) && e.Key == Key.N) { OpenInPrivateTab(); e.Handled = true; }
else if (mods == (ModifierKeys.Control | ModifierKeys.Shift) && e.Key == Key.M) { ToggleCompactMode(); e.Handled = true; } // T3-13
else if (mods == (ModifierKeys.Control | ModifierKeys.Alt) && e.Key == Key.N) { ToggleNotes(); e.Handled = true; } // T3-12
else if (mods == (ModifierKeys.Alt | ModifierKeys.Control) && e.Key == Key.V) { ToggleVerticalTabs(); e.Handled = true; }
else if (mods == (ModifierKeys.Alt | ModifierKeys.Control) && e.Key == Key.H) { ToggleSplit(); e.Handled = true; }
else if (mods == ModifierKeys.Control && e.Key == Key.U) { ViewSource(); e.Handled = true; }
@ -1243,6 +1273,20 @@ public partial class MainWindow : Window
_palette.Register("미디어 재생목록", "playlist play media 음악 영상", () => { PlaylistPopup.IsOpen = true; });
_palette.Register("재생목록 재생", "playlist play start", OpenPlayer);
_palette.Register("기기 연결 (QR 페어링)", "remote pair qr mobile phone", ShowPairing);
// ---- T3 잔여(데스크톱) ----
_palette.Register("이 사이트 흑백", "grayscale filter page action 흑백", () => ApplyPageFilter(PageFilter.Grayscale)); // T3-6
_palette.Register("이 사이트 세피아", "sepia filter page action 세피아", () => ApplyPageFilter(PageFilter.Sepia));
_palette.Register("페이지 액션 끄기", "filter off reset", () => ApplyPageFilter(PageFilter.None));
_palette.Register("노트 패널", "notes note 메모", ToggleNotes); // T3-12
_palette.Register("노트: 현재 페이지 추가", "note add", () => NotesAddPage_Click(this, new RoutedEventArgs()));
_palette.Register("컴팩트 모드 토글", "compact chrome hide ui", ToggleCompactMode); // T3-13
_palette.RegisterDynamic(() => _workspaces // T3-1: 사이트→워크스페이스 규칙
.Select(w => new PaletteCommand($"라우팅 규칙: 이 사이트 → '{w.Name}'", "routing rule workspace",
() => AddRoutingRuleForCurrentSite(w.Name))).ToList());
_palette.RegisterDynamic(() => _tabArchive.Search("") // T3-2: 아카이브 복구
.Take(8)
.Select(a => new PaletteCommand($"아카이브에서 열기: {a.Title}", $"archive reopen {a.Url}",
() => { var t = _tabs.NewTab(); Cv2Navigate(t, a.Url); })).ToList());
_palette.Register("원격 접근 켜기/끄기", "remote server toggle lan", async () => await ToggleRemoteAccess());
_palette.RegisterSeparator();
_palette.RegisterDynamic(() => _tabs.Tabs
@ -1304,6 +1348,112 @@ public partial class MainWindow : Window
Log(!cur.ForceDark ? "이 사이트 강제 다크 켜짐" : "이 사이트 강제 다크 꺼짐");
}
// ---- T3-6 페이지 액션(흑백/세피아 — Boost 통합) ----
internal void ApplyPageFilter(PageFilter filter)
{
var url = Cv?.Source;
if (string.IsNullOrEmpty(url)) { Log("페이지가 없습니다"); return; }
var cur = _boostStore.Get(url);
_boostStore.Set(url, cur.ForceDark, cur.CustomCss, filter);
_views?.ApplyBoostNow(_tabs.ActiveTab!);
Log(filter switch
{
PageFilter.Grayscale => "이 사이트 흑백 적용",
PageFilter.Sepia => "이 사이트 세피아 적용",
_ => "페이지 액션 해제",
});
}
// ---- T3-1 도메인 라우팅 규칙 ----
private void AddRoutingRuleForCurrentSite(string workspaceName)
{
var url = Cv?.Source;
if (string.IsNullOrWhiteSpace(url) || !Uri.TryCreate(url, UriKind.Absolute, out var u))
{ Log("페이지가 없습니다"); return; }
var host = u.Host;
if (host.StartsWith("www.", StringComparison.OrdinalIgnoreCase)) host = host[4..];
_routingRules.Add(host, workspaceName);
Log($"라우팅 규칙 추가: {host} → '{workspaceName}' (다음 이동부터 자동 전환)");
}
// ---- T3-2 아카이브 복구 보조 ----
private static void Cv2Navigate(TabItem tab, string url)
{
try { tab.View?.CoreWebView2?.Navigate(url); } catch { }
}
// ---- T3-12 노트 패널 ----
internal void ToggleNotes()
{
RefreshNotesList();
NotesPopup.IsOpen = true;
}
private void RefreshNotesList()
{
NotesList.ItemsSource = _notes.List()
.Select(n => $"[{n.CreatedAt:MM-dd HH:mm}] {n.Title} — {n.Body}")
.ToList();
NotesList.DisplayMemberPath = null;
}
internal void NotesAddPage_Click(object sender, RoutedEventArgs e)
{
var url = Cv?.Source ?? "";
var title = _tabs.ActiveTab?.Title ?? url;
_notes.Add(title, url);
Log("노트에 페이지 추가됨");
RefreshNotesList();
}
private void NotesDelete_Click(object sender, RoutedEventArgs e)
{
var idx = NotesList.SelectedIndex;
var list = _notes.List();
if (idx >= 0 && idx < list.Count)
{
_notes.Delete(list[idx].Id);
RefreshNotesList();
}
}
// ---- T3-13 컴팩트 모드(크롬 숨김 + 상단 엣지 호버 복원) ----
internal void ToggleCompactMode()
{
_compactMode = !_compactMode;
_chromeRevealed = false;
ApplyChromeVisibility();
Log(_compactMode ? "컴팩트 모드: 크롤 숨김 (상단 가장자리에 마우스를 올리면 복원)" : "컴팩트 모드 해제");
}
private void ApplyChromeVisibility()
{
var chromeRows = new[] { 0, 1, 2 }; // 툴바/북마크바/탭스트립
foreach (var i in chromeRows)
MainGrid.RowDefinitions[i].Height = (_compactMode && !_chromeRevealed) ? new GridLength(0) : GridLength.Auto;
if (!_compactMode) _chromeRevealed = false;
}
protected override void OnPreviewMouseMove(System.Windows.Input.MouseEventArgs e)
{
if (_compactMode)
{
var p = e.GetPosition(this);
var wantReveal = p.Y <= 4; // 상단 엣지 4px
if (wantReveal != _chromeRevealed)
{
_chromeRevealed = wantReveal;
ApplyChromeVisibility();
}
else if (_chromeRevealed && p.Y > 120) // 크롬 영역 벗어나면 다시 숨김
{
_chromeRevealed = false;
ApplyChromeVisibility();
}
}
base.OnPreviewMouseMove(e);
}
// ---- T2-9 스크린샷 ----
private async Task TakeScreenshot()
{
@ -1490,7 +1640,7 @@ public partial class MainWindow : Window
if (cv == null) return;
var tiles = _bookmarks.Take(18).Select(b =>
$"<a href='{System.Net.WebUtility.HtmlEncode(b.Url)}' style='display:block;text-decoration:none;background:#262736;color:#e6e6ec;border-radius:12px;padding:18px 12px;text-align:center;font-size:12.5px;min-width:110px' onmouseover='this.style.background=#2f3042' onmouseout='this.style.background=#262736'><div style='font-size:26px;margin-bottom:8px'>&#127760;</div><div style='overflow:hidden;text-overflow:ellipsis;white-space:nowrap'>{System.Net.WebUtility.HtmlEncode(b.Title)}</div></a>").ToList();
var dial = "<!DOCTYPE html><html><head><meta charset='utf-8'><style>body{background:#14151d;color:#e6e6ec;font-family:'Segoe UI',sans-serif;margin:0;padding:48px 24px}h1{font-size:20px;text-align:center;margin-bottom:28px;font-weight:600}.grid{display:flex;flex-wrap:wrap;gap:14px;justify-content:center;max-width:760px;margin:0 auto}.hint{text-align:center;color:#8a8aa0;font-size:12px;margin-top:36px}a{color:inherit}</style></head><body><h1>새 탭</h1><div class='grid'>" + string.Join("", tiles) + $"</div><div class='hint'>주소창에 검색어나 URL 을 입력하세요 · 즐겨찾기 {_bookmarks.Count}개 중 {tiles.Count}개 표시</div></body></html>";
var dial = "<!DOCTYPE html><html><head><meta charset='utf-8'><style>body{background:#14151d;color:#e6e6ec;font-family:'Segoe UI Variable Text','Segoe UI','Malgun Gothic',sans-serif;margin:0;padding:48px 24px}h1{font-size:20px;text-align:center;margin-bottom:28px;font-weight:600}.grid{display:flex;flex-wrap:wrap;gap:14px;justify-content:center;max-width:760px;margin:0 auto}.hint{text-align:center;color:#9aa0b4;font-size:12.5px;margin-top:36px}a{color:inherit}</style></head><body><h1>새 탭</h1><div class='grid'>" + string.Join("", tiles) + $"</div><div class='hint'>주소창에 검색어나 URL 을 입력하세요 · 즐겨찾기 {_bookmarks.Count}개 중 {tiles.Count}개 표시</div></body></html>";
cv.NavigateToString(dial);
}
@ -1575,6 +1725,7 @@ public partial class MainWindow : Window
{
ExitSplit();
_tabs.SwitchWorkspace(ws);
_currentWorkspaceName = ws.Name; // T3-1 라우팅 기준
Log($"워크스페이스 '{ws.Name}' 전환 ({ws.Urls.Count} 탭)");
}