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