fix(settings): sidebar_visible/width 영속화 — IPC + store hydration 추가

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) <noreply@anthropic.com>
This commit is contained in:
th-kim0823
2026-05-15 11:23:54 +09:00
parent cc11ee8cad
commit da6d296b77
6 changed files with 39 additions and 6 deletions

View File

@@ -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();

View File

@@ -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'),

View File

@@ -152,7 +152,14 @@ export const useInbox = create<InboxState>((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<InboxState>((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() {

View File

@@ -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 }>;

View File

@@ -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(<App />);
await screen.findByRole('tab', { name: /Inbox/ });
// Sidebar renders an <aside> element when visible
expect(document.querySelector('aside')).not.toBeNull();
// loadInitial 비동기 hydrate 가 완료될 때까지 기다림
await waitFor(() => expect(document.querySelector('aside')).not.toBeNull());
});
});

View File

@@ -19,7 +19,9 @@ vi.mock('../../src/renderer/inbox/api.js', () => ({
listRecallCandidate: vi.fn(async () => null),
countsByStatus: mockCountsByStatus,
getSettings: vi.fn(async () => ({ ai_enabled: true })),
listByStatus: mockListByStatus
listByStatus: mockListByStatus,
setSidebarVisible: vi.fn(async () => ({ ok: true as const })),
setSidebarWidth: vi.fn(async () => ({ ok: true as const }))
},
notebookApi: {
list: vi.fn(async () => [