o2o-negosium-original/landing/app/components/sections/how-it-works-demo.tsx
Haewon Kam 3214e4f782 [feat] landing: 가이드 2번 탭 mp4 연결 · 최종 CTA 에 결과 화면 배치
① 이용 가이드 2번 탭 (협력사별 1:1 협상)
video 필드가 빠져 있어 이 탭만 1.7MB GIF(390x780)를 내려받고 있었다. 같은 내용의
mp4 가 저장소에 있는데 참조만 없었다 — 786K, 540x1080 으로 더 가볍고 더 크다.
GifSlot 이 video 를 우선 재생하므로 한 줄로 끝난다.

원본 캡쳐가 좌우로 잘려 문장이 중간에 끊기는 문제는 남아 있다(540x1080 원본 파일
자체에 잘린 채로 들어 있어 CSS 로 못 고친다). 재촬영이 필요하고 주석에 남겼다.

③ 최종 CTA — negotiation_annotated_3d.mp4
이용 가이드에는 넣지 않았다. 그 섹션은 "실제 화면으로 보여드립니다" 라고 약속하는데
이 영상은 재구성 목업이다(실제 UI 는 "아이마켓코리아 (구매 MD)", 영상은 "아이마켓 구매 MD").
1·3번 탭이 실제 캡쳐인데 2번만 목업이면 register 도 어긋난다.
전환을 요청하는 자리에는 그 약속이 없으므로 마케팅 컴포짓이 맞는 자리다.

- 영상 배경이 밝은 회색(#EEF4F5)이라 무대 위에 그냥 놓으면 구멍처럼 뜬다.
  모서리를 깎고 그림자를 깔아 "떠 있는 물체"로 만들면 밝기 차이가 의도로 읽힌다.
- 폰 프레임이 영상에 내장돼 있어 PhoneFrame 으로 감싸지 않는다. 감싸면 이중 프레임이다.
- 2단이 되면서 width md(max-w-4xl) → lg(max-w-5xl), 제목은 lg 부터 한 단계 축소.
  4xl 안에 세로 목업과 카피를 밀어넣으면 양쪽 다 좁아진다.
- preload="none" — 페이지 최하단이라 첫 로드에 677K 를 미리 받을 이유가 없다.

검증: 1280x900 2단 정상, 375x812 카피→영상 중앙 적층·가로 넘침 없음.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 11:56:59 +09:00

292 lines
11 KiB
TypeScript

import { useState } from "react"
import { AnimatePresence, motion } from "motion/react"
import { Check, Monitor, Smartphone, type LucideIcon } from "lucide-react"
import { BrowserFrame, PhoneFrame } from "@/components/ui/device-mockups"
import { Section } from "@/components/ui/section"
import { CONTROL_GAP, SectionHeading } from "@/components/ui/section-heading"
import { Typography } from "@/components/ui/typography"
/** 3단계 이용 가이드 — 탭 전환식 GIF 시연 (데스크톱/모바일 목업). */
export function HowItWorksDemo() {
const [activeKey, setActiveKey] = useState<DemoTab["key"]>("create")
const activeTab = DEMO_TABS.find((tab) => tab.key === activeKey)!
return (
<Section id="how-it-works-demo" width="lg" bordered className="overflow-hidden">
<SectionHeading
align="center"
eyebrow="How it Works"
title="견적 생성부터 낙찰까지 한눈에 보기"
description="견적을 열고 협상이 끝나기까지, 실제 화면으로 보여드립니다."
descriptionClassName="max-w-2xl"
/>
{/* 탭 스위치 — 비교 섹션 토글과 같은 간격 규칙(본문 쪽에 묶인다) */}
<div className={`flex justify-center ${CONTROL_GAP}`}>
<div className="p-1 bg-fill rounded-control inline-flex flex-wrap justify-center gap-1">
{DEMO_TABS.map((tab) => (
<button
key={tab.key}
onClick={() => setActiveKey(tab.key)}
className={`px-5 py-2.5 rounded-control text-xs sm:text-sm font-bold transition-all flex items-center gap-2 cursor-pointer ${
activeKey === tab.key ? "bg-white text-primary" : "text-ink-soft hover:text-ink"
}`}
>
<tab.tabIcon className="w-4 h-4" />
<span>{tab.tabLabel}</span>
</button>
))}
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12 items-center min-h-[500px]">
{/* 좌: 단계 설명 */}
<div className="lg:col-span-5 space-y-6 text-center lg:text-left">
<AnimatePresence mode="wait">
<motion.div
key={`${activeTab.key}-text`}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
transition={{ duration: 0.4 }}
className="space-y-6"
>
{/* 알약 배경 + 아이콘을 뺀 글자만의 라벨.
레퍼런스 라벨은 11px/600/자간 normal 에 배경도 아이콘도 없다. */}
<div className={`text-[11px] font-semibold ${activeTab.badge.className}`}>{activeTab.badge.label}</div>
<Typography variant="heading">{activeTab.heading}</Typography>
{activeTab.paragraphs.map((paragraph, idx) => (
<Typography key={idx} variant="body">
{paragraph}
</Typography>
))}
<div className="border-t border-line pt-5 mt-2 space-y-3">
{activeTab.checks.map((check, idx) => (
<div key={idx} className="flex items-center gap-2 text-xs font-bold text-ink-soft justify-center lg:justify-start">
<Check className={`w-4 h-4 ${activeTab.checkClassName}`} />
<span>{check}</span>
</div>
))}
</div>
</motion.div>
</AnimatePresence>
</div>
{/* 우: 디바이스 목업 + GIF */}
<div className="lg:col-span-7 flex justify-center items-center">
<AnimatePresence mode="wait">
<motion.div
key={`${activeTab.key}-mockup`}
initial={{ opacity: 0, scale: 0.96, y: 15 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.96, y: -15 }}
transition={{ duration: 0.4 }}
className={`w-full ${activeTab.mockup === "phone" ? "flex justify-center" : ""}`}
>
{activeTab.mockup === "browser" ? (
<BrowserFrame url={activeTab.url!}>
<GifSlot tab={activeTab} />
</BrowserFrame>
) : (
<PhoneFrame>
<GifSlot tab={activeTab} />
</PhoneFrame>
)}
</motion.div>
</AnimatePresence>
</div>
</div>
</Section>
)
}
/** GIF 로드 실패 시(파일 미배치) 배치 안내 폴백을 보여주는 슬롯. */
function GifSlot({ tab }: { tab: DemoTab }) {
const [error, setError] = useState(false)
if (error) {
const phone = tab.mockup === "phone"
return (
<div className={phone ? "text-center p-6 space-y-4 w-full" : "text-center p-8 max-w-sm space-y-4"}>
<div className={`${tab.fallback.iconWrapClassName} flex items-center justify-center mx-auto animate-pulse`}>
<tab.fallback.icon className={phone ? "w-6 h-6" : "w-7 h-7"} />
</div>
<div>
<h4 className={`${phone ? "text-xs" : "text-sm"} font-bold text-ink break-keep`}>{tab.fallback.title}</h4>
<p className={`${phone ? "text-[10px] px-2" : "text-[11px]"} text-ink-muted font-semibold mt-1 leading-relaxed break-keep`}>
{" "}
<code className={`px-1.5 py-0.5 rounded border ${tab.fallback.codeClassName}`}>public{tab.gif}</code> {" "}
{tab.fallback.hint}
</p>
</div>
</div>
)
}
// video(mp4)가 있으면 우선 재생 — 줌 스크린캐스트는 GIF보다 용량 1/5·화질 우위. 없으면 GIF.
if (tab.video) {
return (
<video
src={tab.video}
poster={tab.poster}
autoPlay
muted
loop
playsInline
onError={() => setError(true)}
className="w-full h-full object-cover"
/>
)
}
return (
<img
src={tab.gif}
alt={tab.gifAlt}
onError={() => setError(true)}
referrerPolicy="no-referrer"
className="w-full h-full object-cover"
/>
)
}
type DemoTab = {
key: "create" | "negotiate" | "result"
tabIcon: LucideIcon
tabLabel: string
badge: { label: string; className: string }
checkClassName: string
heading: React.ReactNode
paragraphs: React.ReactNode[]
checks: string[]
mockup: "browser" | "phone"
url?: string
gif: string
video?: string
poster?: string
gifAlt: string
fallback: {
icon: LucideIcon
iconWrapClassName: string
codeClassName: string
title: string
hint: string
}
}
const DEMO_TABS: DemoTab[] = [
{
key: "create",
tabIcon: Monitor,
tabLabel: "1. 견적 · 기준 수립",
badge: { label: "구매 관리자 콘솔", className: "text-primary" },
checkClassName: "text-primary",
heading: (
<>
<br />
</>
),
paragraphs: [
<>
, <b> (LPS)</b> {" "}
<b className="text-primary"> .</b>
</>,
<>
, {" "}
<b className="text-primary"> (Target Price)</b> .
</>,
],
checks: ["인터넷 최저가(LPS) 시세 상시 반영", "마진을 지키는 최적 목표 단가 자동 완성"],
mockup: "browser",
url: "https://console.negotium.ai/estimates/new",
gif: "/gifs/quote_generation.gif",
video: "/gifs/quote_generation.mp4",
poster: "/gifs/quote_generation.jpg",
gifAlt: "견적 생성 가이드라인 수립 시연",
fallback: {
icon: Monitor,
iconWrapClassName: "w-14 h-14 bg-primary-soft rounded-card text-primary",
codeClassName: "bg-white text-primary",
title: "견적 생성 시연 GIF 공간",
hint: "실제 기동 GIF를 배치하시면 이 영역에 자동으로 재생됩니다.",
},
},
{
key: "negotiate",
tabIcon: Smartphone,
tabLabel: "2. 협력사별 1:1 협상",
badge: { label: "협력사 협상 포털", className: "text-positive" },
checkClassName: "text-positive",
heading: (
<>
<br />
·
</>
),
paragraphs: [
<>
<b> </b> , .
</>,
<>
. ,
.
</>,
],
checks: ["협력사별 1:1 비대면 협상 포털", "같은 기준으로 응대해 관계 부담 없음"],
mockup: "phone",
/* mp4 를 붙이기 전까지 이 탭만 1.7MB GIF(390x780)를 내려받고 있었다. 같은 내용의
mp4 가 저장소에 있는데 참조가 빠져 있었다 — 786K, 540x1080 으로 더 가볍고 더 크다.
(원본 캡쳐가 좌우로 잘려 문장이 끊기는 문제는 남아 있다. 재촬영이 필요하다.) */
gif: "/gifs/negotiation.gif",
video: "/gifs/negotiation.mp4",
gifAlt: "협력사 포털 모바일 협상 화면",
fallback: {
icon: Smartphone,
iconWrapClassName: "w-12 h-12 bg-positive-soft border border-positive/15 rounded-full text-positive",
codeClassName: "bg-fill text-positive",
title: "협상 시연 GIF 공간",
hint: "실제 모바일 협상 시뮬레이션 GIF를 배치해 주세요.",
},
},
{
key: "result",
tabIcon: Monitor,
tabLabel: "3단계: AI 협상 결과 확인",
badge: { label: "구매 관리자 콘솔", className: "text-accent" },
checkClassName: "text-accent",
heading: (
<>
<br />
</>
),
paragraphs: [
<>
, <b> </b> .
</>,
<>
<b> </b>,
.
</>,
],
checks: ["낙찰 결과·절감액 자동 집계", "누적 절감액·협상 통계 대시보드"],
mockup: "browser",
url: "https://console.negotium.ai/dashboard/reports",
gif: "/gifs/negotiation_result.gif",
video: "/gifs/negotiation_result.mp4",
poster: "/gifs/negotiation_result.jpg",
gifAlt: "협상 결과 분석 대시보드 시연",
fallback: {
icon: Monitor,
iconWrapClassName: "w-14 h-14 bg-accent-soft rounded-card text-accent",
codeClassName: "bg-white text-accent",
title: "협상 결과 확인 시연 GIF 공간",
hint: "결과 보고 대시보드 기동 GIF를 배치하시면 이 영역에 자동으로 재생됩니다.",
},
},
]