[feat] landing: 데모 영상 3종 실화면 재촬영(라이브 협상·견적생성·협상결과, 줌인 연출) + 도입문의·데모요청 폼 mailto 임시연결

This commit is contained in:
Mina Choi 2026-08-12 17:04:46 +09:00
parent 1a859b198c
commit 8112394aef
13 changed files with 33 additions and 27 deletions

View File

@ -175,9 +175,9 @@ export function Contact() {
<div className="w-12 h-12 bg-primary text-white rounded-full flex items-center justify-center mx-auto mb-6"> <div className="w-12 h-12 bg-primary text-white rounded-full flex items-center justify-center mx-auto mb-6">
<Check className="w-6 h-6" strokeWidth={2.5} /> <Check className="w-6 h-6" strokeWidth={2.5} />
</div> </div>
<h3 className="text-[24px] font-semibold tracking-[-0.02em] text-ink mb-3">도입 문의 신청이 접수되었습니다</h3> <h3 className="text-[24px] font-semibold tracking-[-0.02em] text-ink mb-3">메일 앱에서 문의 내용을 전송해 주세요</h3>
<p className="text-ink-soft text-[15px] leading-[1.6] max-w-md mx-auto mb-8"> <p className="text-ink-soft text-[15px] leading-[1.6] max-w-md mx-auto mb-8">
{formData.contactName} 님께 영업일 기준 하루 안에 연락드리겠습니다. 도입 문의 메일 작성 창을 열었습니다. 내용을 확인하고 전송해 주시면, {formData.contactName} 님께 영업일 기준 하루 안에 연락드리겠습니다.
</p> </p>
<Button <Button
type="button" type="button"

View File

@ -238,11 +238,11 @@ const DEMO_TABS: DemoTab[] = [
], ],
checks: ["협력사별 1:1 비대면 협상 포털", "같은 기준으로 응대해 관계 부담 없음"], checks: ["협력사별 1:1 비대면 협상 포털", "같은 기준으로 응대해 관계 부담 없음"],
mockup: "phone", mockup: "phone",
/* mp4 를 붙이기 전까지 이 탭만 1.7MB GIF(390x780)를 내려받고 있었다. 같은 내용의 /* 현행 공급사 포털(토스풍 UI)에서 실제로 진행한 협상 스크린캐스트. 390x780 모바일 뷰포트로
mp4 가 저장소에 있는데 참조가 빠져 있었다 — 786K, 540x1080 으로 더 가볍고 더 크다. 인트로→가격 밀당(협상 카드 4장)→타결(18,480원 합의)까지, 1.5배속. gif 는 무JS 폴백(폭 228). */
(원본 캡쳐가 좌우로 잘려 문장이 끊기는 문제는 남아 있다. 재촬영이 필요하다.) */
gif: "/gifs/negotiation.gif", gif: "/gifs/negotiation.gif",
video: "/gifs/negotiation.mp4", video: "/gifs/negotiation.mp4",
poster: "/gifs/negotiation.jpg",
gifAlt: "협력사 포털 모바일 협상 화면", gifAlt: "협력사 포털 모바일 협상 화면",
fallback: { fallback: {
icon: Smartphone, icon: Smartphone,

View File

@ -125,12 +125,10 @@ export function DemoRequestModal({ open, onClose }: { open: boolean; onClose: ()
<Check className="w-6 h-6" /> <Check className="w-6 h-6" />
</div> </div>
<Typography variant="cardTitle" as="h2" id={titleId} className="mb-3"> <Typography variant="cardTitle" as="h2" id={titleId} className="mb-3">
데모 신청이 접수되었습니다 메일 앱에서 요청을 전송해 주세요
</Typography> </Typography>
{/* 폼에서 "Demo를 보내드립니다"라고 약속했으므로 성공 화면도 같은 약속을 지킨다.
여기서 "일정을 잡아 연락드립니다"로 새면 방문자가 무엇을 기다려야 하는지 모른다. */}
<Typography variant="small"> <Typography variant="small">
입력하신 이메일로 사용해보실 수 있는 Demo를 보내드립니다. 데모 요청 메일 작성 창을 열었습니다. 전송해 주시면 입력하신 이메일로 사용해보실 수 있는 Demo를 보내드립니다.
</Typography> </Typography>
</div> </div>
) : ( ) : (

View File

@ -1,16 +1,13 @@
/* /*
* 리드 전송 — 데모 요청 모달과 상담 신청 폼이 함께 쓴다. * 리드 전송 — 데모 요청 모달과 상담 신청 폼이 함께 쓴다.
* *
* 목적지는 같은 오리진의 서버리스 함수(api/lead.ts)다. 랜딩은 ssr:false 정적 빌드지만 * [임시] 백엔드/서버리스 연결 전까지는 mailto 로 방문자의 메일 앱을 열어 문의를 보내게 한다.
* Vercel 의 /api 디렉터리 함수는 별개로 배포되므로 서버 없이도 POST 를 받을 수 있다. * 메일 앱이 '열릴' 뿐 실제 전송 여부는 알 수 없으므로, UI 도 "접수 완료"가 아니라
* 같은 오리진이라 엔드포인트 환경변수도, CORS 도 필요 없다. * "메일 앱을 열었습니다 — 전송해 주세요"로 정직하게 안내한다(fake success 금지). 나중에 API/시트로 교체.
*
* 성공을 위조하지 않는다. 예전 상담 신청 폼은 1.2초 뒤 성공 화면만 띄우고 아무 데도
* 보내지 않았다 — 화면은 "접수되었습니다"인데 영업이 받을 리드는 없었다.
* 여기서는 서버가 리드를 확보했을 때만 ok 를 돌려준다.
*/ */
const ENDPOINT = "/api/lead" // 리드 수신 주소 — 임시. 실제 영업 주소로 바꾸고, 이후 API 붙이면 mailto 자체를 교체할 것.
const LEAD_EMAIL = "o2odev@o2o.kr"
export type LeadSource = "demo-request" | "contact" export type LeadSource = "demo-request" | "contact"
@ -36,15 +33,26 @@ export function isEmailLike(value: string) {
export async function submitLead(lead: Lead): Promise<LeadResult> { export async function submitLead(lead: Lead): Promise<LeadResult> {
if (!lead.name.trim() || !isEmailLike(lead.email)) return { ok: false, reason: "invalid" } if (!lead.name.trim() || !isEmailLike(lead.email)) return { ok: false, reason: "invalid" }
try { const label = lead.source === "demo-request" ? "데모 요청" : "도입 문의"
const res = await fetch(ENDPOINT, { const body = [
method: "POST", `[${label}]`,
headers: { "Content-Type": "application/json" }, `회사명: ${lead.company || "-"}`,
body: JSON.stringify(lead), `담당자: ${lead.name}`,
}) `이메일: ${lead.email}`,
return res.ok ? { ok: true } : { ok: false, reason: "rejected" } `연락처: ${lead.phone || "-"}`,
} catch { `내용: ${lead.message || "-"}`,
// 로컬 dev(react-router dev)에는 /api 가 없어서 여기로 온다. 확인은 `vercel dev` 로. ].join("\n")
return { ok: false, reason: "network" } const href = `mailto:${LEAD_EMAIL}?subject=${encodeURIComponent(`[${label}] ${lead.company || lead.name}`)}&body=${encodeURIComponent(body)}`
// 방문자의 메일 앱을 연다 — 앵커 클릭이 location.href 할당보다 핸들러 발동이 확실하다.
// 실제 전송은 방문자가 하므로 성공을 단정하지 않는다(호출부 UI 는 안내 문구).
if (typeof document !== "undefined") {
const a = document.createElement("a")
a.href = href
a.style.display = "none"
document.body.appendChild(a)
a.click()
a.remove()
} }
return { ok: true }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 42 KiB