From da6d296b77cf36e7eaa50c876e8c64e024bc95f4 Mon Sep 17 00:00:00 2001 From: th-kim0823 Date: Fri, 15 May 2026 11:23:54 +0900 Subject: [PATCH] =?UTF-8?q?fix(settings):=20sidebar=5Fvisible/width=20?= =?UTF-8?q?=EC=98=81=EC=86=8D=ED=99=94=20=E2=80=94=20IPC=20+=20store=20hyd?= =?UTF-8?q?ration=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit final code review 의 Important issue 대응. SettingsService 의 setSidebarVisible/ setSidebarWidth getter/setter 는 이미 있었지만 IPC handler + store hydration missing 으로 매 launch 시 사이드바 닫힌 상태로 시작하던 회귀. - settings:set-sidebar-visible / set-sidebar-width IPC 핸들러 추가 - InboxApi.getSettings 응답에 sidebar_visible/sidebar_width 포함 - preload 의 setSidebarVisible/setSidebarWidth invoke 노출 - store.loadInitial 가 settings.sidebar_visible/sidebar_width 로 hydrate - store.toggleSidebar 가 IPC 호출하여 영속화 - test mock 에 setSidebarVisible/setSidebarWidth 추가 Co-Authored-By: Claude Opus 4.7 (1M context) --- src/main/ipc/settingsApi.ts | 11 +++++++++++ src/preload/index.ts | 3 +++ src/renderer/inbox/store.ts | 13 +++++++++++-- src/shared/types.ts | 5 +++++ tests/unit/App.test.tsx | 9 ++++++--- tests/unit/store.notebook.test.ts | 4 +++- 6 files changed, 39 insertions(+), 6 deletions(-) diff --git a/src/main/ipc/settingsApi.ts b/src/main/ipc/settingsApi.ts index 063194b..5b1bf59 100644 --- a/src/main/ipc/settingsApi.ts +++ b/src/main/ipc/settingsApi.ts @@ -112,6 +112,17 @@ export function registerSettingsApi(deps?: SettingsIpcDeps): void { return { ok: true as const }; }); + // v0.4 — Sidebar UI state 영속화 + ipcMain.handle('settings:set-sidebar-visible', async (_e, visible: boolean) => { + await settings.setSidebarVisible(visible); + return { ok: true as const }; + }); + + ipcMain.handle('settings:set-sidebar-width', async (_e, width: number) => { + await settings.setSidebarWidth(width); + return { ok: true as const }; + }); + ipcMain.handle('settings:set-sync-auto-enabled', async (_e, value: boolean) => { await deps.settings.setAutoSyncEnabled(value); await deps.syncTimer?.reconfigure(); diff --git a/src/preload/index.ts b/src/preload/index.ts index 503e92c..1ba7c6f 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -81,6 +81,9 @@ const api: InklingApi = { getSettings: () => ipcRenderer.invoke('settings:get'), setAiEnabled: (enabled: boolean) => ipcRenderer.invoke('settings:set-ai-enabled', enabled), setOnboardingCompleted: (completed: boolean) => ipcRenderer.invoke('settings:set-onboarding-completed', completed), + // v0.4 — Sidebar UI state 영속화 + setSidebarVisible: (visible: boolean) => ipcRenderer.invoke('settings:set-sidebar-visible', visible), + setSidebarWidth: (width: number) => ipcRenderer.invoke('settings:set-sidebar-width', width), // v0.2.9 Cut B Task 16 — disabled 메모 재투입 + count. enqueueDisabled: () => ipcRenderer.invoke('inbox:enqueue-disabled'), getDisabledCount: () => ipcRenderer.invoke('inbox:get-disabled-count'), diff --git a/src/renderer/inbox/store.ts b/src/renderer/inbox/store.ts index 0569ef8..e8ba3bd 100644 --- a/src/renderer/inbox/store.ts +++ b/src/renderer/inbox/store.ts @@ -152,7 +152,14 @@ export const useInbox = create((set, get) => ({ inboxApi.countsByStatus({ notebookId }), inboxApi.getSettings() ]); - set({ notes, continuity, pendingCount, ollamaStatus, todayCount, trashCount, expiredCandidates, failedCount, recallCandidate, counts, ai_enabled: settings.ai_enabled ?? true, loading: false }); + set({ + notes, continuity, pendingCount, ollamaStatus, todayCount, trashCount, expiredCandidates, + failedCount, recallCandidate, counts, + ai_enabled: settings.ai_enabled ?? true, + sidebarVisible: settings.sidebar_visible ?? false, + sidebarWidth: settings.sidebar_width ?? 240, + loading: false + }); } catch (e) { // 첫 launch 의 IPC 실패 (DB migration 실패 / main process 비정상) 시 무한 loading 회피. // 빈 데이터로 진입하면 사용자가 캡처 시도 → 실제 fail 이 표면화 → 재시도 가능. @@ -480,7 +487,9 @@ export const useInbox = create((set, get) => ({ await get().refreshMeta(); }, toggleSidebar() { - set({ sidebarVisible: !get().sidebarVisible }); + const next = !get().sidebarVisible; + set({ sidebarVisible: next }); + void inboxApi.setSidebarVisible(next); }, // v0.4 Task 11 — promotion candidate actions. async loadPromotionCandidates() { diff --git a/src/shared/types.ts b/src/shared/types.ts index 2fb75b1..6e50f9f 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -228,8 +228,13 @@ export interface InboxApi { vision_model?: string | null; vision_capable_cache?: string[]; vision_cache_at?: string; + // v0.4 — Sidebar UI state 영속화 + sidebar_visible?: boolean; + sidebar_width?: number; }>; setAiEnabled(enabled: boolean): Promise<{ ok: true }>; + setSidebarVisible(visible: boolean): Promise<{ ok: true }>; + setSidebarWidth(width: number): Promise<{ ok: true }>; setOnboardingCompleted(completed: boolean): Promise<{ ok: true }>; // v0.2.9 Cut B Task 16 — ai_status='disabled' 메모 재투입 (사용자가 ai_enabled OFF→ON 전환 시). enqueueDisabled(): Promise<{ count: number }>; diff --git a/tests/unit/App.test.tsx b/tests/unit/App.test.tsx index 0111a18..c96d529 100644 --- a/tests/unit/App.test.tsx +++ b/tests/unit/App.test.tsx @@ -56,6 +56,8 @@ vi.mock('../../src/renderer/inbox/api.js', () => ({ // v0.2.9 Cut B Task 12 — onboarding wizard 분기. default 는 onboarding_completed=true 라 wizard 미표시. getSettings: vi.fn(async () => ({ onboarding_completed: true })), setAiEnabled: vi.fn(async () => ({ ok: true as const })), + setSidebarVisible: vi.fn(async () => ({ ok: true as const })), + setSidebarWidth: vi.fn(async () => ({ ok: true as const })), setOnboardingCompleted: vi.fn(async () => ({ ok: true as const })), // v0.2.9 Cut B Task 16 — AiProviderSection 가 SettingsPage 렌더 시 호출. getDisabledCount: vi.fn(async () => 0), @@ -174,11 +176,12 @@ describe('App header — 3 tabs (v0.4)', () => { }); it('Sidebar 컴포넌트가 렌더 트리에 포함됨 (sidebarVisible=true)', async () => { - useInbox.setState({ sidebarVisible: true, notebooks: [] }); + // loadInitial 의 getSettings 가 sidebar_visible=true 반환 (Strict Mode 중복 호출 대비 mockResolvedValue). + vi.mocked(inboxApi.getSettings).mockResolvedValue({ onboarding_completed: true, sidebar_visible: true }); render(); await screen.findByRole('tab', { name: /Inbox/ }); - // Sidebar renders an