/* ============================================================================ * 서명전에 — iOS Analyzing (fullscreen progress) * ========================================================================== */ function AnalyzingScreen({ onCancel, onDone, scenario = "jeonse", pending = false }) { const STEPS = [ { id: "upload", label: "파일 올리는 중" }, { id: "ocr", label: "글씨 읽는 중 (OCR)" }, { id: "parse", label: "조항 구조 살펴보는 중" }, { id: "risk", label: "위험 조항 찾는 중" }, { id: "redflag", label: "전세사기 체크리스트 점검" }, { id: "compile", label: "리포트 준비하는 중" }, ]; const [idx, setIdx] = useState(0); const [pct, setPct] = useState(0); /* pending 이 길어질 때 보여줄 안심 sub-message — 5초마다 회전. LLM 응답이 8~15초 걸리는 게 정상이라, 이 시간 동안 "멈춘 거 아냐?" 하는 불안을 줄여준다. */ const [reassureIdx, setReassureIdx] = useState(0); const REASSURE_MSGS = [ "리포트 준비하는 중…", "꼼꼼하게 살펴보고 있어요…", "복잡한 조항이 많네요…", "거의 다 됐어요…", ]; /* pending 값은 항상 최신으로 읽도록 ref에 보관 */ const pendingRef = useRef(pending); useEffect(() => { pendingRef.current = pending; }, [pending]); /* pending 동안 5초마다 sub-message 회전. pending=false 가 되면 즉시 멈추고 인덱스 0 으로 리셋. */ useEffect(() => { if (!pending) { setReassureIdx(0); return; } const t = setInterval(() => { setReassureIdx((i) => (i + 1) % REASSURE_MSGS.length); }, 5000); return () => clearInterval(t); }, [pending]); useEffect(() => { let cancelled = false; let p = 0; const tick = () => { if (cancelled) return; const isPending = pendingRef.current; /* pending 이어도 96% 까지 가도록 cap 상향 (92→96). 마지막 4% 는 거의 안 움직이는 듯한 미세 진행을 보여서 "정지" 인상 제거. 사용자 피드백: "리포트 준비하는 중에서 멈춰있는 거 천천히 진행해서 멈추지 않고 쭉 진행하게" — 진행 곡선을 더 길게 늘여서 마지막 step 에 빨리 도달하지 않게, 도달해도 미세하게 계속 차오르게. */ const cap = isPending ? 96 : 100; let delta; if (!isPending) { /* 응답 도착 → 남은 구간을 빠르게 채워서 자연스럽게 마무리. */ delta = p < 60 ? 5.0 : p < 85 ? 3.5 : p < 95 ? 2.5 : 1.5; } else { /* pending: 초반은 빠르게, 후반은 매우 천천히 — 그래도 항상 0보다 큼. - 0~30%: 빠르게 (파일 업로드·OCR 시작 단계 자연스러움) - 30~60%: 보통 (구조 분석) - 60~80%: 느리게 (위험 조항·체크리스트) - 80~92%: 매우 느리게 (리포트 준비 도달 직전) - 92~96%: 거의 멈춘 듯 미세 진행 (멈춤 인상 방지) */ delta = p < 30 ? 2.5 : p < 60 ? 1.4 : p < 80 ? 0.55 : p < 92 ? 0.18 : 0.05; } p = Math.min(cap, p + delta); setPct(p); const targetI = Math.min(STEPS.length - 1, Math.floor(p / (100 / STEPS.length))); setIdx(targetI); if (p >= 100) { setTimeout(() => !cancelled && onDone?.(), 240); } else { setTimeout(tick, 70); } }; setTimeout(tick, 150); return () => { cancelled = true; }; }, []); const size = 140; const stroke = 8; const r = (size - stroke) / 2; const circ = 2 * Math.PI * r; const dash = circ * (pct / 100); return (
{pending && idx === STEPS.length - 1 ? REASSURE_MSGS[reassureIdx] : `${STEPS[idx]?.label}…`}