/* ============================================================================ * 서명전에 — 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 (
{/* % 숫자 표시는 사용자가 "멈춘 것 같다" 고 인식 → 통째로 제거. 대신 ring 자체가 indeterminate(360° 회전 + dash 흐름) 로 항상 살아있다. pct=100 도달하면 회전 멈추고 체크 아이콘으로 변환. */}
{/* 배경 트랙 */} {/* 회전하는 호 — 항상 돈다. pct=100 이면 정적 ring 으로 마무리. */} {pct < 100 ? ( ) : ( )} {/* 가운데 — 100% 면 체크, 아니면 빈 상태 (메시지는 ring 아래 title/sub 가 담당) */} {pct >= 100 && ( )}

분석 중이에요

{/* pending 이고 마지막 step 인 경우엔 안심 sub-message 회전, 아니면 step label */}

{pending && idx === STEPS.length - 1 ? REASSURE_MSGS[reassureIdx] : `${STEPS[idx]?.label}…`}

{STEPS.map((s, i) => { const state = i < idx ? "done" : i === idx ? "active" : "idle"; /* 전체 진행률(pct, 0~100)을 스텝 수로 나눠서 각 스텝의 개별 진행률을 계산. - 이전 스텝: 100% - 현재 스텝: 해당 구간 안에서 0~100 보간 - 다음 스텝: 0% */ const segment = 100 / STEPS.length; let stepPct; if (state === "done") { stepPct = 100; } else if (state === "active") { stepPct = Math.max(0, Math.min(100, ((pct - i * segment) / segment) * 100)); } else { stepPct = 0; } return (
{state === "done" ? ( ) : state === "active" ? ( ) : ( )} {s.label} {/* % 숫자는 제거 — 정적으로 보여 "멈춤" 인상 줌. done/active/idle 상태 아이콘만 남김. */}
); })}
); } window.AnalyzingScreen = AnalyzingScreen; Object.assign(window, { AnalyzingScreen });