전수 audit 후 핵심 root fix 3 + edge cases 5: ROOT - inbox:set-status IPC 가 pushNoteUpdated emit (이전엔 stale → 호출처별 refreshMeta 필요) - upsertNote 가 current view status 인식 (이전엔 잘못된 status 노트 잔류) - store async 함수 try/catch (이전엔 IPC fail 시 무한 loading) EDGE - restoreNote 가 status='active' 도 갱신 - upsertNote trash 판정 deletedAt → status='trashed' - Modal Escape dismiss 통일 (5개 modal) - OnboardingWizard IPC fail fallback (try/catch + skip) - MoveStatusModal overlay 클릭 close Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
79 lines
3.6 KiB
TypeScript
79 lines
3.6 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { inboxApi } from '../api.js';
|
|
|
|
/**
|
|
* v0.2.9 Cut B Task 11 — 첫 launch onboarding 위저드.
|
|
*
|
|
* 3 옵션 (AI 사용 / 원문만 / 나중에) 중 하나를 선택. AI 옵션 (true/false) 은
|
|
* setAiEnabled 로 settings 에 저장, 모든 옵션은 setOnboardingCompleted(true) 로
|
|
* 두 번째 launch 부터 미노출. "나중에" 는 ai_enabled 기본값 (true) 유지 — 사용자
|
|
* 가 SettingsPage 에서 추후 선택 가능.
|
|
*/
|
|
export function OnboardingWizard({ onClose }: { onClose: () => void }): React.ReactElement {
|
|
const [busy, setBusy] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
async function choose(aiEnabled: boolean | null): Promise<void> {
|
|
setBusy(true);
|
|
setError(null);
|
|
try {
|
|
if (aiEnabled !== null) await inboxApi.setAiEnabled(aiEnabled);
|
|
await inboxApi.setOnboardingCompleted(true);
|
|
onClose();
|
|
} catch (e) {
|
|
// IPC 실패 (예: settings 저장 throw) 시 modal stuck 방지. 사용자에게 메시지 표시 +
|
|
// "지금 건너뛰기" 로 fallback 길 제공. choose() 가 throw 하지 않고 무한 wizard 잠금
|
|
// 회피.
|
|
setError((e as Error).message);
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
}
|
|
|
|
function skip(): void {
|
|
// IPC 자체가 실패하는 상태 → ai_enabled 변경/onboarding flag 저장 모두 포기하고 wizard 만 닫기.
|
|
// 다음 launch 에 다시 wizard 가 뜸 (onboarding_completed=false 상태). 그래도 사용자가
|
|
// 진입 자체는 가능.
|
|
onClose();
|
|
}
|
|
|
|
// Escape key 로 wizard 종료 (skip 동일 — onboarding flag 미저장).
|
|
useEffect(() => {
|
|
function onKey(e: KeyboardEvent) {
|
|
if (e.key === 'Escape') skip();
|
|
}
|
|
document.addEventListener('keydown', onKey);
|
|
return () => document.removeEventListener('keydown', onKey);
|
|
}, []);
|
|
|
|
return (
|
|
<div role="dialog" aria-label="시작 안내" style={{
|
|
position: 'fixed', top: 0, left: 0, right: 0, bottom: 0,
|
|
background: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000
|
|
}}>
|
|
<div style={{ background: '#fff', padding: 24, borderRadius: 8, maxWidth: 520 }}>
|
|
<h2 style={{ margin: '0 0 12px' }}>Inkling 사용 시작</h2>
|
|
<p style={{ fontSize: 14, lineHeight: 1.6, marginBottom: 12 }}>
|
|
Inkling 은 로컬 LLM (Ollama) 으로 메모를 자동 정리합니다.
|
|
Ollama 가 설치되어 있고 한국어 지원 모델 (gemma3, gemma2 등) 이 pull 되어 있어야 최적의 경험이 가능합니다.
|
|
</p>
|
|
<p style={{ fontSize: 13, marginBottom: 16 }}>
|
|
설치 가이드:
|
|
<a href="https://ollama.com/download" target="_blank" rel="noopener noreferrer">ollama.com/download</a>
|
|
</p>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
<button onClick={() => choose(true)} disabled={busy}>AI 자동 처리 사용 (Ollama 필요)</button>
|
|
<button onClick={() => choose(false)} disabled={busy}>원문만 저장 (AI 처리 안 함)</button>
|
|
<button onClick={() => choose(null)} disabled={busy} style={{ marginTop: 4 }}>나중에 설정</button>
|
|
</div>
|
|
{error !== null && (
|
|
<div style={{ marginTop: 12, padding: 8, background: '#fdecea', color: '#a3261c', fontSize: 12, borderRadius: 4 }}>
|
|
<div>설정 저장 실패: {error}</div>
|
|
<button onClick={skip} style={{ marginTop: 8 }}>지금 건너뛰기</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|