// Loading.jsx — full-screen loading splash that doubles as a session/data prefetch. // onDone is called as ({ user, threads, settings }) once the server confirms the // session and ships initial payloads. onAuthFailed is called if the token is bad. function Loading({ onDone, onAuthFailed }) { const initialSteps = [ { pct: 12, msg: '커널 부팅 · canon_lm_server' }, { pct: 28, msg: '세션 토큰 검증' }, { pct: 52, msg: '대화 목록 불러오는 중' }, { pct: 78, msg: '설정 불러오는 중' }, { pct: 100, msg: '준비 완료.' }, ]; const [i, setI] = React.useState(0); const [steps, setSteps] = React.useState(initialSteps); const [err, setErr] = React.useState(null); React.useEffect(() => { let cancelled = false; const advance = (idx, msg) => { if (!cancelled) { setSteps(s => { const c = s.slice(); if (msg) c[idx] = { ...c[idx], msg }; return c; }); setI(idx); } }; (async () => { try { // Step 0: health advance(0); try { await window.API.health(); } catch (e) { throw new Error("Can't reach server at " + window.API.serverUrl); } // Step 1: /auth/me advance(1); let user; try { user = await window.API.me(); } catch (e) { if (e.status === 401) { onAuthFailed && onAuthFailed(); return; } throw e; } // Step 2: threads advance(2); let threads = []; try { threads = await window.API.listThreads(); } catch (_) { /* empty list is fine */ } // Step 3: settings advance(3, threads.length ? 'Loading settings · ' + threads.length + ' thread' + (threads.length === 1 ? '' : 's') : 'Loading settings'); let settings = null; try { settings = await window.API.getSettings(); } catch (_) {} // Step 4: done advance(4); await new Promise(r => setTimeout(r, 220)); if (!cancelled && onDone) onDone({ user, threads, settings }); } catch (e) { if (!cancelled) setErr(e.message || String(e)); } })(); return () => { cancelled = true; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const cur = steps[i] || steps[0]; return (