/* ============================================================================ * 서명전에 — 계약서 텍스트 붙여넣기 (paste) 스크린 * 카메라·파일 업로드 외의 3번째 입력 경로. AppsInToss 환경에서는 getClipboardText * 로 클립보드를 자동으로 읽어 textarea 를 선채우고, 웹(브라우저)에서는 사용자 * 제스처로 붙여넣기를 유도한다. * * 호출 규약: * onBack() — 취소/뒤로가기 * onSubmit(text: string) — "분석하기" 확정. 부모가 /analyze-text 로 POST. * ========================================================================== */ function PasteScreen({ onBack, onSubmit }) { const [text, setText] = useState(""); const [busy, setBusy] = useState(false); /* 클립보드 자동 로딩 상태: * "idle" → 아직 시도 안 함 * "loading" → 읽는 중 (깜빡임 방지용) * "loaded" → 성공 (textarea 에 붙음) * "empty" → 성공했지만 클립보드가 비어 있음 * "denied" → 권한 거부 → 직접 붙여넣기 안내 * "unsupported"→ 어댑터가 readText 를 지원하지 않음 (웹 폴백 필요) */ const [clipState, setClipState] = useState("idle"); const textareaRef = useRef(null); const didAutoLoadRef = useRef(false); // 어댑터 레이어 참조. 없으면 웹 브라우저 폴백으로만 동작. const adapter = (typeof window !== "undefined" && window.TossAdapter) || null; const isInToss = !!adapter?.isInAppsInToss?.(); /* 클립보드에서 텍스트 읽기. 성공 시 textarea 에 넣고, 실패 사유별로 상태를 * 분기해서 안내 문구를 결정한다. * * - 앱인토스 네이티브: getClipboardText 호출 (사용자 제스처 불필요) * - 웹 브라우저: navigator.clipboard.readText 호출 (사용자 제스처 필요; * 제스처 없이 호출하면 NotAllowedError → "denied" 로 분기) */ const loadFromClipboard = async () => { if (!adapter?.clipboard?.readText) { setClipState("unsupported"); return; } setClipState("loading"); try { const clip = await adapter.clipboard.readText(); const got = (clip || "").toString(); if (got.trim().length === 0) { setClipState("empty"); return; } setText(got); setClipState("loaded"); // 다음 틱에 focus — 즉시 focus 하면 일부 브라우저에서 커서가 맨 앞으로 튐 setTimeout(() => { if (textareaRef.current) { try { textareaRef.current.focus(); const len = textareaRef.current.value.length; textareaRef.current.setSelectionRange(len, len); } catch (_) { /* 무시 */ } } }, 0); } catch (err) { // TossPermissionError / NotAllowedError 계열 const name = err?.name || err?.constructor?.name || ""; if ( (adapter.TossPermissionError && err instanceof adapter.TossPermissionError) || name === "NotAllowedError" || /denied|permission/i.test(err?.message || "") ) { setClipState("denied"); } else if (adapter.TossNotSupportedError && err instanceof adapter.TossNotSupportedError) { setClipState("unsupported"); } else { // 기타 오류는 조용히 denied 로 취급 (사용자가 직접 붙여넣기 하면 됨) setClipState("denied"); } } }; /* 스크린 첫 진입 시 한 번만 자동 로딩. * 앱인토스 환경에서는 제스처 없이도 읽을 수 있으므로 자동으로 수행. * 브라우저에서는 사용자 제스처가 필요하므로 자동 로딩을 건너뛰고, 화면의 * "클립보드에서 붙여넣기" 버튼을 누를 때만 시도한다. */ useEffect(() => { if (didAutoLoadRef.current) return; didAutoLoadRef.current = true; if (isInToss) { loadFromClipboard(); } }, []); // eslint-disable-line const charCount = text.length; // 너무 짧은 텍스트는 분석해도 결과가 의미없음. 50자 이하면 경고. const tooShort = charCount > 0 && charCount < 50; // 너무 긴 텍스트는 서버 비용/타임아웃 위험. 100k 초과 시 경고. const tooLong = charCount > 100000; const canSubmit = !busy && charCount >= 50 && !tooLong; const handleSubmit = async () => { if (!canSubmit) return; setBusy(true); try { await onSubmit(text.trim()); } catch (err) { // 에러는 부모에서 alert 처리. 여기서는 busy 만 풀어준다. console.warn("[paste] onSubmit failed:", err); } finally { setBusy(false); } }; // 상태별 안내 배너 내용 const banner = (() => { if (clipState === "loading") { return { tone: "info", text: "클립보드를 읽는 중이에요…" }; } if (clipState === "loaded" && charCount > 0) { return { tone: "ok", text: "클립보드에 있던 내용을 붙여넣었어요. 필요하면 고쳐 주세요." }; } if (clipState === "empty") { return { tone: "info", text: "클립보드가 비어 있어요. 계약서 본문을 복사한 뒤 다시 눌러 주세요." }; } if (clipState === "denied") { return { tone: "warn", text: "클립보드 읽기 권한이 없어요. 아래 입력창을 길게 눌러 '붙여넣기' 하거나 ⌘V / Ctrl+V 로 붙여 주세요.", }; } if (clipState === "unsupported" && !isInToss) { return { tone: "info", text: "브라우저에서는 입력창에 직접 붙여넣어 주세요. 길게 누르기 또는 ⌘V / Ctrl+V 로 붙일 수 있어요.", }; } return null; })(); const bannerStyle = (tone) => ({ padding: "10px 14px", borderRadius: 12, fontSize: 13.5, lineHeight: 1.45, margin: "0 16px 10px", background: tone === "ok" ? "var(--safe-soft)" : tone === "warn" ? "var(--warn-soft)" : "var(--fill-4, rgba(120,120,128,0.08))", color: tone === "ok" ? "var(--safe-ink)" : tone === "warn" ? "var(--warn-ink)" : "var(--label, #111)", border: tone === "ok" ? "0.5px solid color-mix(in srgb, var(--safe) 35%, transparent)" : tone === "warn" ? "0.5px solid color-mix(in srgb, var(--warn) 35%, transparent)" : "0.5px solid var(--separator, rgba(60,60,67,0.12))", }); return (