import { useEffect, useId, useRef, useState } from "react" import { AnimatePresence, motion, useReducedMotion } from "motion/react" import { Check, Loader2, X } from "lucide-react" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Typography } from "@/components/ui/typography" import { EASE_OUT_EXPO } from "@/lib/motion" import { isEmailLike, submitLead } from "@/lib/lead" /* * 데모 요청 모달 — 이름·이메일만 받는 리드 캡처. * * 상담 신청(contact.tsx)은 회사명·담당자·연락처·문의내용까지 받는 고관여 폼이다. * 이건 그 앞단이다. 필드가 늘수록 이탈이 늘기 때문에 두 칸에서 멈춘다 — 영업이 * 첫 연락을 하는 데 필요한 최소가 이름과 이메일이고, 나머지는 통화에서 채운다. * * 전체 페이지로 보내지 않고 모달로 띄우는 이유도 같다. 히어로에서 관심이 생긴 * 순간에 그 자리에서 받아야 한다. 스크롤로 내려보내면 그 사이에 식는다. */ export function DemoRequestModal({ open, onClose }: { open: boolean; onClose: () => void }) { const [name, setName] = useState("") const [email, setEmail] = useState("") const [website, setWebsite] = useState("") // 봇 함정 const [status, setStatus] = useState<"idle" | "submitting" | "success" | "error">("idle") const dialogRef = useRef(null) const firstFieldRef = useRef(null) const restoreFocusTo = useRef(null) const reduce = useReducedMotion() const titleId = useId() /* 열릴 때마다 초기화. 남겨두면 성공 화면이 다시 뜨거나 이전 에러가 붙어 나온다. */ useEffect(() => { if (!open) return setStatus("idle") restoreFocusTo.current = document.activeElement as HTMLElement | null const t = setTimeout(() => firstFieldRef.current?.focus(), 60) return () => clearTimeout(t) }, [open]) /* 배경 스크롤 잠금. 모달 뒤에서 페이지가 움직이면 모달이 페이지의 일부처럼 읽힌다. */ useEffect(() => { if (!open) return const prev = document.body.style.overflow document.body.style.overflow = "hidden" return () => { document.body.style.overflow = prev restoreFocusTo.current?.focus?.() } }, [open]) /* Esc 로 닫기 + Tab 을 모달 안에 가둔다. 가두지 않으면 뒤 페이지의 링크로 포커스가 빠져나가고, 키보드·스크린리더 사용자는 자기가 어디 있는지 알 수 없게 된다. */ useEffect(() => { if (!open) return const onKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") { onClose() return } if (e.key !== "Tab") return const focusables = dialogRef.current?.querySelectorAll( 'button:not([disabled]), input:not([disabled]), a[href]', ) if (!focusables?.length) return const first = focusables[0] const last = focusables[focusables.length - 1] if (e.shiftKey && document.activeElement === first) { e.preventDefault() last.focus() } else if (!e.shiftKey && document.activeElement === last) { e.preventDefault() first.focus() } } document.addEventListener("keydown", onKeyDown) return () => document.removeEventListener("keydown", onKeyDown) }, [open, onClose]) const valid = name.trim().length > 0 && isEmailLike(email) const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() if (!valid || status === "submitting") return setStatus("submitting") const result = await submitLead({ source: "demo-request", name, email, website }) setStatus(result.ok ? "success" : "error") } return ( {open && (
{status === "success" ? (
데모 신청이 접수되었습니다 {/* 폼에서 "Demo를 보내드립니다"라고 약속했으므로 성공 화면도 같은 약속을 지킨다. 여기서 "일정을 잡아 연락드립니다"로 새면 방문자가 무엇을 기다려야 하는지 모른다. */} 입력하신 이메일로 사용해보실 수 있는 Demo를 보내드립니다.
) : ( <> 데모 요청 성함과 이메일만 남겨주시면, 사용해보실 수 있는 Demo를 보내드립니다!
{/* 봇 함정. 화면에서 감추되 display:none 은 쓰지 않는다 — 일부 봇은 숨겨진 필드를 걸러낸다. 스크린리더에는 aria-hidden + tabIndex 로 숨긴다. */}
setWebsite(e.target.value)} />
setName(e.target.value)} disabled={status === "submitting"} />
setEmail(e.target.value)} disabled={status === "submitting"} />
{status === "error" && (

지금은 접수가 어렵습니다.{" "} 상담 신청 으로 남겨주시면 동일하게 연락드립니다.

)}
)}
)}
) }