diff --git a/landing/app/components/sections/hero-dataflow.tsx b/landing/app/components/sections/hero-dataflow.tsx
index 9d24cf2..eee2c8d 100644
--- a/landing/app/components/sections/hero-dataflow.tsx
+++ b/landing/app/components/sections/hero-dataflow.tsx
@@ -1,6 +1,8 @@
+import { useState } from "react"
import { ArrowRight } from "lucide-react"
import { Button } from "@/components/ui/button"
+import { DemoRequestModal } from "@/components/ui/demo-request-modal"
import { CTA_GAP, DISPLAY_LEAD_GAP } from "@/components/ui/section-heading"
import { Typography } from "@/components/ui/typography"
@@ -18,6 +20,8 @@ import { Typography } from "@/components/ui/typography"
* - 카피는 화면 중앙 정렬. 밴드 개념이 없으니 상하로 나눌 이유도 없다.
*/
export function HeroDataFlow() {
+ const [demoOpen, setDemoOpen] = useState(false)
+
return (
+ {/* 2차 CTA 는 상담 신청(#contact-section)과 목적지가 겹쳐 사실상 같은 버튼이었다.
+ 데모 요청으로 갈라서, 고관여 폼(상담 신청) 앞단에 저관여 리드 캡처를 둔다. */}
-
+
+ setDemoOpen(false)} />
)
}
diff --git a/landing/app/components/sections/negotiation-demo.tsx b/landing/app/components/sections/negotiation-demo.tsx
index 651fd3e..a68416d 100644
--- a/landing/app/components/sections/negotiation-demo.tsx
+++ b/landing/app/components/sections/negotiation-demo.tsx
@@ -129,16 +129,22 @@ export function NegotiationDemo() {
아이브로우 간격만 20px(다른 섹션은 12px)이었다 — 프리미티브를 우회하면
같은 관계에 두 번째 값이 생긴다. */}
{/* 두 문장은 의미 단위가 다르므로 줄을 명시적으로 나눈다.
- 자동 줄바꿈에 맡기면 "에이전트는 기준 / 밖으로 안 나갑니다" 처럼 끊긴다. */}
+ 자동 줄바꿈에 맡기면 어절이 어색하게 끊긴다.
+
+ 예전 카피는 "에이전트는 정해진 기준을 벗어나지 않습니다" 였다. FAQ 2번
+ ("에이전트가 우리 기준을 벗어나 합의해 버리면요?")을 헤드라인으로 끌어올린
+ 것이었는데, 아직 생기지도 않은 반론에 가장 비싼 한 줄을 쓰고 있었다.
+ 게다가 부정문이라 안 하는 일을 약속했다 — 체험을 권하는 자리에는 맞지 않는다.
+ 방어는 FAQ 에 그대로 남아 있으니 여기서는 능력을 말한다. */}
- 직접 협상해 보세요.
+ 사용할수록 진화하는 에이전틱 협상,
- 에이전트는 정해진 기준을 벗어나지 않습니다.
+ 직접 경험해 보세요!
>
}
/>
diff --git a/landing/app/components/ui/demo-request-modal.tsx b/landing/app/components/ui/demo-request-modal.tsx
new file mode 100644
index 0000000..87e90dc
--- /dev/null
+++ b/landing/app/components/ui/demo-request-modal.tsx
@@ -0,0 +1,212 @@
+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 [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({ name, email })
+ setStatus(result.ok ? "success" : "error")
+ }
+
+ return (
+
+ {open && (
+
+
+
+
+
+
+
+ {status === "success" ? (
+
+
+
+
+
+ 데모 신청이 접수되었습니다
+
+
+ 담당자가 이메일로 일정과 준비 사항을 안내해 드립니다.
+
+