import React, { useEffect, useState } from 'react'; import { useInbox, selectFilteredNotes } from './store.js'; import { inboxApi } from './api.js'; import { isRecoveryDismissedToday, markRecoveryDismissed } from './recoveryToast.js'; import { NoteCard } from './components/NoteCard.js'; import { ContinuityBadge } from './components/ContinuityBadge.js'; import { IdentityCounter } from './components/IdentityCounter.js'; import { PendingBanner } from './components/PendingBanner.js'; import { OllamaBanner } from './components/OllamaBanner.js'; import { RecoveryToast } from './components/RecoveryToast.js'; import { TagUndoToast } from './components/TagUndoToast.js'; import { ExpiryBanner } from './components/ExpiryBanner.js'; import { FailedBanner } from './components/FailedBanner.js'; import { RecallBanner } from './components/RecallBanner.js'; import { SettingsPage } from './components/SettingsPage.js'; import { OnboardingWizard } from './components/OnboardingWizard.js'; import { SearchBox } from './components/SearchBox.js'; import { ReviewView } from './components/ReviewView.js'; import { Sidebar } from './components/Sidebar.js'; import { PromotionBanner } from './components/PromotionBanner.js'; import { BatchMoveModal } from './components/BatchMoveModal.js'; import type { InboxView } from './store.js'; // QuickCapture 단축키 modifier — macOS 는 Cmd, 그 외는 Ctrl. const MOD_KEY = /Mac/i.test(navigator.platform) ? 'Cmd' : 'Ctrl'; export function App(): React.ReactElement { const { notes, trashNotes, trashCount, showTrash, loading, loadInitial, refreshMeta, upsertNote, removeNote, continuity, tagFilter, setTagFilter, toggleShowTrash, restoreNote, permanentDeleteNote, emptyTrash } = useInbox(); const showSettings = useInbox((s) => s.showSettings); const setShowSettings = useInbox((s) => s.setShowSettings); // v0.2.9 Cut B Task 5 — 4탭 (Inbox/완료/보관/휴지통). const view = useInbox((s) => s.view); const counts = useInbox((s) => s.counts); const setView = useInbox((s) => s.setView); const searchResults = useInbox((s) => s.searchResults); const selectedNotebookId = useInbox((s) => s.selectedNotebookId); const notebooks = useInbox((s) => s.notebooks); const runBatchClassify = useInbox((s) => s.runBatchClassify); const batchClassifying = useInbox((s) => s.batchClassifying); const [recoveryDismissed, setRecoveryDismissed] = useState(isRecoveryDismissedToday()); // v0.2.9 Cut B Task 12 — 첫 launch onboarding 분기. null = 로딩, true = 표시, false = 미표시. const [showOnboarding, setShowOnboarding] = useState(null); useEffect(() => { void (async () => { try { const settings = await inboxApi.getSettings(); setShowOnboarding(!settings.onboarding_completed); } catch { // 안전한 fallback — settings 읽기 실패 시 wizard 미표시 (기존 사용자 무영향). setShowOnboarding(false); } })(); }, []); useEffect(() => { void useInbox.getState().loadNotebooks(); void useInbox.getState().loadPromotionCandidates(); }, []); useEffect(() => { const isMac = /Mac/i.test(navigator.platform); const handler = (e: KeyboardEvent) => { if (e.key === 'b' && (isMac ? e.metaKey : e.ctrlKey) && !e.shiftKey && !e.altKey) { e.preventDefault(); useInbox.getState().toggleSidebar(); } }; window.addEventListener('keydown', handler); return () => window.removeEventListener('keydown', handler); }, []); useEffect(() => { void loadInitial(); const unsubNote = inboxApi.onNoteUpdated((note) => { upsertNote(note); void refreshMeta(); }); const unsubOllama = inboxApi.onOllamaStatus((status) => { useInbox.setState({ ollamaStatus: status }); }); const unsubNav = inboxApi.onNavigate((view) => { // v0.2.9 Cut B Task 4 — setView 가 mirror state (showTrash/showSettings) 동기 갱신. useInbox.getState().setView(view); }); const onFocus = () => { void refreshMeta(); }; window.addEventListener('focus', onFocus); return () => { unsubNote(); unsubOllama(); unsubNav(); window.removeEventListener('focus', onFocus); }; // onOllamaStatus 콜백은 useInbox.setState 직접 호출 — store reference 가 안정적이라 // deps array 에 추가 불필요. mount 시 1회 구독 + unmount 시 해제. }, [loadInitial, refreshMeta, upsertNote]); // v0.4 T5 — default notebook(첫 번째) 선택 시 "AI 정리하기" 버튼 노출 조건. const isDefaultNotebook = notebooks.length > 0 && selectedNotebookId === notebooks[0]?.id; if (showOnboarding === null) return <>; if (showOnboarding) return setShowOnboarding(false)} />; if (view === 'review-daily') return ; if (view === 'review-weekly') return ; if (view === 'review-monthly') return ; if (showSettings) return ; const showRecovery = continuity.showRecoveryToast && !recoveryDismissed; const filtered = selectFilteredNotes({ notes, tagFilter }); const displayed = searchResults !== null ? searchResults : filtered; const tabBtnStyle = (active: boolean): React.CSSProperties => ({ background: active ? '#0a4b80' : 'transparent', color: active ? '#fff' : '#0a4b80', border: '1px solid #0a4b80', borderRadius: 4, padding: '4px 10px', fontSize: 12, cursor: 'pointer' }); return (

Inkling

{( [ { key: 'inbox', label: 'Inbox', count: counts.active }, { key: 'completed', label: '완료', count: counts.completed }, { key: 'trash', label: '휴지통', count: counts.trashed } ] as const ).map((t) => ( ))}
{view === 'inbox' && isDefaultNotebook && notebooks.length > 1 && ( )}
{!showTrash && ( <> {/* AI/만료/회상 배너는 active 노트 컨텍스트 — inbox 탭에서만 노출. completed/archived 에서는 무관 컨텐츠라 숨김. */} {view === 'inbox' && ( <> setShowSettings(true)} /> { markRecoveryDismissed(); setRecoveryDismissed(true); }} /> )} {tagFilter !== null && (
🔎 필터: #{tagFilter} ({filtered.length}개)
)} {loading && notes.length === 0 ? (
불러오는 중…
) : searchResults !== null && displayed.length === 0 ? (
검색 결과가 없습니다.
) : notes.length === 0 ? (
머릿속에 떠다니는 한 줄을 적어보세요. {MOD_KEY}+Shift+J
) : displayed.length === 0 ? (
이 태그의 노트가 없습니다.
) : ( displayed.map((n) => ( removeNote(n.id)} onUpdated={(u) => upsertNote(u)} /> )) )} )} {showTrash && ( <>
{trashCount === 0 ? '휴지통이 비어있습니다.' : `${trashCount}개 보관 중`}
{trashNotes.length === 0 ? null : ( trashNotes.map((n) => ( upsertNote(u)} onRestore={() => void restoreNote(n.id)} onPermanentDelete={() => void permanentDeleteNote(n.id)} /> )) )} )}
); }