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