[feat] landing: 히어로/데모 리뉴얼 — 콘솔형 히어로·price-sparkline 추가, 데모 미디어(mp4/jpg) 세트 추가, ROI 시뮬레이터·negotiation-console·버튼·홈 라우트 수정

This commit is contained in:
Mina Choi 2026-07-22 08:29:54 +09:00
parent 6ffdb9a389
commit e9427543c8
20 changed files with 178 additions and 81 deletions

View File

@ -19,9 +19,9 @@
--font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
/* 브랜드 */
--color-primary: #3182F6;
--color-primary-deep: #1B64DA; /* hover */
--color-primary-soft: #E8F3FF; /* 연한 파랑 배경 — 봇 말풍선, 배지 */
--color-primary: #476EFF;
--color-primary-deep: #2F52E0; /* hover */
--color-primary-soft: #ECF0FF; /* 연한 파랑 배경 — 봇 말풍선, 배지 */
/* 텍스트. ink 는 다크 섹션에선 배경색으로도 쓴다 */
--color-ink: #191F28;
@ -38,6 +38,10 @@
--color-line-strong: #E5E8EB; /* 카드 테두리 */
--color-positive: #0B8F57; /* 낙찰·성공 강조 */
--color-positive-soft: #E8F7EF; /* 연한 초록 배경 — 낙찰 배지 */
--color-accent: #8F52FF; /* 보라 강조 — 결과 탭·히어로 장식 */
--color-accent-soft: #F4EFFF; /* 연한 보라 배경 */
}
html {

View File

@ -0,0 +1,51 @@
import { motion } from "motion/react"
import { ArrowDown, ArrowRight } from "lucide-react"
import { Button } from "@/components/ui/button"
/** 히어로 변형 — 실시간 협상 콘솔 시즐 영상을 전면에 세운 풀와이드 히어로. */
export function HeroConsole() {
return (
<section className="relative min-h-screen bg-surface-neu text-ink flex flex-col justify-center pt-32 pb-20 overflow-hidden">
<div className="mesh-bg absolute top-[-260px] left-[-160px] opacity-40 pointer-events-none" />
<div className="max-w-6xl mx-auto px-6 w-full relative z-10">
<motion.div
initial={{ opacity: 0, y: 28, scale: 0.98 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
transition={{ duration: 0.8, delay: 0.1 }}
className="rounded-[28px] overflow-hidden border border-line-strong shadow-[0_30px_80px_rgba(20,25,45,0.12)] bg-white"
>
<video
src="/gifs/hero_console.mp4"
poster="/gifs/hero_console.jpg"
autoPlay
muted
loop
playsInline
className="w-full h-auto block"
/>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.35 }}
className="flex flex-col sm:flex-row justify-center gap-5 pt-10"
>
<Button
href="#contact-section"
className="w-full sm:w-auto group shadow-[0_8px_24px_rgba(49,130,246,0.15)] hover:scale-[1.02] active:scale-[0.98]"
>
<span> </span>
<ArrowRight className="w-5 h-5 transition-transform group-hover:translate-x-1" />
</Button>
<Button href="#how-it-works" variant="glass" className="w-full sm:w-auto gap-1.5">
<span> </span>
<ArrowDown className="w-4 h-4 text-ink-soft" />
</Button>
</motion.div>
</div>
</section>
)
}

View File

@ -11,8 +11,8 @@ export function HeroGlassmorphic() {
<section className="relative min-h-screen bg-surface text-ink flex flex-col justify-center pt-32 overflow-hidden">
{/* 흐릿한 파스텔 블롭 배경 */}
<div className="absolute top-[10%] right-[15%] w-[480px] h-[480px] bg-primary/10 rounded-full filter blur-[120px] pointer-events-none" />
<div className="absolute bottom-[10%] left-[10%] w-[450px] h-[450px] bg-[#8F52FF]/10 rounded-full filter blur-[120px] pointer-events-none" />
<div className="absolute top-[30%] left-[30%] w-[380px] h-[380px] bg-sky-300/10 rounded-full filter blur-[100px] pointer-events-none" />
<div className="absolute bottom-[10%] left-[10%] w-[450px] h-[450px] bg-accent/10 rounded-full filter blur-[120px] pointer-events-none" />
<div className="absolute top-[30%] left-[30%] w-[380px] h-[380px] bg-primary/10 rounded-full filter blur-[100px] pointer-events-none" />
{/* 저채도 그리드 패턴 오버레이 */}
<div className="absolute inset-0 bg-[linear-gradient(rgba(0,0,0,0.01)_1px,transparent_1px),linear-gradient(90deg,rgba(0,0,0,0.01)_1px,transparent_1px)] bg-[size:40px_40px] pointer-events-none" />
@ -27,7 +27,7 @@ export function HeroGlassmorphic() {
<motion.div
animate={{ rotate: -360 }}
transition={{ duration: 80, repeat: Infinity, ease: "linear" }}
className="absolute bottom-[18%] left-[6vw] lg:left-[12%] w-28 h-28 sm:w-48 sm:h-48 border-2 border-dashed border-[#8F52FF]/30 rounded-full pointer-events-none z-0"
className="absolute bottom-[18%] left-[6vw] lg:left-[12%] w-28 h-28 sm:w-48 sm:h-48 border-2 border-dashed border-accent/30 rounded-full pointer-events-none z-0"
/>
<motion.div
animate={{ x: [0, 10, 0], y: [0, 10, 0] }}

View File

@ -2,14 +2,13 @@ import { motion } from "motion/react"
import { ArrowDown, ArrowRight } from "lucide-react"
import { Button } from "@/components/ui/button"
import { PhoneFrame } from "@/components/ui/device-mockups"
import { Typography } from "@/components/ui/typography"
/** 대안 히어로 — 뉴모피즘 배경 + 실제 모바일 협상 화면(GIF) 폰 목업. */
export function HeroNeumorphic() {
return (
<section className="relative min-h-screen bg-surface-neu text-ink flex flex-col justify-center pt-32 pb-20 overflow-hidden">
<div className="mesh-bg absolute top-[-300px] right-[-100px] opacity-40 pointer-events-none" />
{/* 우측 상단 메시 제거 — 영상 배경(#F2F4F7)과 섹션 배경을 완전 동일하게 유지해 경계선 없이 블렌딩 */}
<div className="max-w-5xl mx-auto px-6 w-full relative z-10 grid grid-cols-1 lg:grid-cols-12 gap-16 items-center">
{/* 좌: 카피 + CTA */}
@ -49,7 +48,7 @@ export function HeroNeumorphic() {
</motion.div>
</div>
{/* 우: 협력사 모바일 협상 실제 화면 (GIF) */}
{/* 우: 모바일 협상 채팅 + 외부 설명 UI(제안가·협상카드·낙찰 하이라이트) 합성 영상 */}
<div className="lg:col-span-5 flex justify-center">
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
@ -57,13 +56,15 @@ export function HeroNeumorphic() {
transition={{ duration: 0.8, delay: 0.2 }}
className="w-full flex justify-center"
>
<PhoneFrame>
<img
src="/gifs/negotiation.gif"
alt="협력사 모바일 협상 포털 실제 화면"
className="w-full h-full object-cover"
/>
</PhoneFrame>
<video
src="/gifs/negotiation_annotated.mp4"
poster="/gifs/negotiation_annotated.jpg"
autoPlay
muted
loop
playsInline
className="w-full max-w-[440px] h-auto"
/>
</motion.div>
</div>
</div>

View File

@ -128,6 +128,22 @@ function GifSlot({ tab }: { tab: DemoTab }) {
)
}
// 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}
@ -151,6 +167,8 @@ type DemoTab = {
mockup: "browser" | "phone"
url?: string
gif: string
video?: string
poster?: string
gifAlt: string
fallback: {
icon: LucideIcon
@ -188,6 +206,8 @@ const DEMO_TABS: DemoTab[] = [
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,
@ -201,8 +221,8 @@ const DEMO_TABS: DemoTab[] = [
key: "negotiate",
tabIcon: Smartphone,
tabLabel: "2단계: AI 자동 밀당 협상",
badge: { icon: Bot, label: "협력사 협상 포털 (웹·모바일)", className: "bg-emerald-50 border border-emerald-100 text-positive" },
checkClassName: "text-emerald-500",
badge: { icon: Bot, label: "협력사 협상 포털 (웹·모바일)", className: "bg-positive-soft border border-positive/15 text-positive" },
checkClassName: "text-positive",
heading: (
<>
<br />
@ -224,7 +244,7 @@ const DEMO_TABS: DemoTab[] = [
gifAlt: "AI 모바일 자동 협상 시연",
fallback: {
icon: Smartphone,
iconWrapClassName: "w-12 h-12 bg-emerald-50 border border-emerald-100 rounded-full text-emerald-500",
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를 배치해 주세요.",
@ -234,8 +254,8 @@ const DEMO_TABS: DemoTab[] = [
key: "result",
tabIcon: Monitor,
tabLabel: "3단계: AI 협상 결과 확인",
badge: { icon: Monitor, label: "구매 관리자 콘솔 (웹)", className: "bg-purple-50 border border-purple-100 text-purple-600" },
checkClassName: "text-purple-500",
badge: { icon: Monitor, label: "구매 관리자 콘솔 (웹)", className: "bg-accent-soft border border-accent/15 text-accent" },
checkClassName: "text-accent",
heading: (
<>
<br />
@ -255,11 +275,13 @@ const DEMO_TABS: DemoTab[] = [
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-purple-50 rounded-2xl text-purple-600",
codeClassName: "bg-white text-purple-600",
iconWrapClassName: "w-14 h-14 bg-accent-soft rounded-2xl text-accent",
codeClassName: "bg-white text-accent",
title: "협상 결과 확인 시연 GIF 공간",
hint: "결과 보고 대시보드 기동 GIF를 배치하시면 이 영역에 자동으로 재생됩니다.",
},

View File

@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from "react"
import { AnimatePresence, motion } from "motion/react"
import { Bot, Handshake, ShieldCheck } from "lucide-react"
import { PriceSparkline } from "@/components/ui/price-sparkline"
import { SlateRenderer } from "@/components/ui/slate-renderer"
import { Typography } from "@/components/ui/typography"
import { EASE_OUT_EXPO } from "@/lib/motion"
@ -105,7 +106,11 @@ export function NegotiationConsole() {
{/* 계단식 하락 스파크라인 + 목표가 기준선 */}
<div>
<PriceSparkline activeStep={activeStep} />
<PriceSparkline
prices={NEGOTIATION_STEPS.map((step) => step.price)}
targetPrice={TARGET_PRICE}
progress={activeStep < 0 ? 0 : (activeStep + 1) / NEGOTIATION_STEPS.length}
/>
<div className="flex justify-between mt-2">
<Typography variant="micro"> {START_PRICE.toLocaleString()}</Typography>
<Typography variant="micro" className="text-primary">
@ -125,7 +130,7 @@ export function NegotiationConsole() {
initial={{ scale: 0.95, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.95, opacity: 0 }}
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-emerald-50 text-positive border border-emerald-100 text-xs font-bold"
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-positive-soft text-positive border border-positive/15 text-xs font-bold"
>
<ShieldCheck className="w-4 h-4 shrink-0" />
{NEGOTIATION_STEPS[activeStep].badge}
@ -220,54 +225,6 @@ export function NegotiationConsole() {
const START_PRICE = 1200000
const TARGET_PRICE = 1080000
/** 대화 진행에 맞춰 그려지는 계단식 가격 하락 라인 + 목표가 점선. */
function PriceSparkline({ activeStep }: { activeStep: number }) {
const W = 100
const H = 44
// 위아래 여백을 둔 가격 범위 매핑
const MAX = 1215000
const MIN = 1035000
const y = (price: number) => ((MAX - price) / (MAX - MIN)) * H
const prices = NEGOTIATION_STEPS.map((step) => step.price)
const stepX = (i: number) => (i / (prices.length - 1)) * W
// step-after 계단 경로
let d = `M 0 ${y(prices[0]).toFixed(1)}`
for (let i = 1; i < prices.length; i++) {
d += ` L ${stepX(i).toFixed(1)} ${y(prices[i - 1]).toFixed(1)} L ${stepX(i).toFixed(1)} ${y(prices[i]).toFixed(1)}`
}
const progress = activeStep < 0 ? 0 : Math.min((activeStep + 1) / prices.length, 1)
return (
<svg viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="none" className="w-full h-16 overflow-visible" aria-hidden>
<defs>
<clipPath id="nego-spark-clip">
<rect x="0" y="-4" width={progress * W} height={H + 8} style={{ transition: "width 0.5s ease-out" }} />
</clipPath>
</defs>
{/* 목표가 기준선 */}
<g className="text-primary/40">
<line
x1="0"
y1={y(TARGET_PRICE)}
x2={W}
y2={y(TARGET_PRICE)}
stroke="currentColor"
strokeWidth="1"
strokeDasharray="4 4"
vectorEffect="non-scaling-stroke"
/>
</g>
{/* 가격 하락 계단 — 진행률만큼 클립으로 드러남 */}
<g clipPath="url(#nego-spark-clip)" className="text-primary">
<path d={d} fill="none" stroke="currentColor" strokeWidth="2.5" vectorEffect="non-scaling-stroke" strokeLinejoin="round" />
</g>
</svg>
)
}
// 데모 대화 시나리오 — 1,200,000원 제시에서 1,050,000원 낙찰까지 6스텝
const NEGOTIATION_STEPS: DialogueStep[] = [
{

View File

@ -77,7 +77,7 @@ export function ROISimulator() {
</div>
<div className="flex gap-3 bg-surface p-5 rounded-2xl border border-line">
<ShieldCheck className="w-5 h-5 text-emerald-500 flex-shrink-0 mt-0.5" />
<ShieldCheck className="w-5 h-5 text-positive flex-shrink-0 mt-0.5" />
<Typography variant="caption" className="text-ink-soft">
(LPS) B2B (3.1% ~ 4.2%
) . .

View File

@ -15,11 +15,12 @@ const buttonVariants = cva(
// 밝은 배경 위 반투명 보조 버튼 (글라스 히어로)
glass: "bg-white/70 hover:bg-white text-ink-soft hover:text-ink border border-white shadow-sm",
},
// radius 는 사이즈 무관 rounded-2xl(16px) 로 통일 — 알약형 금지
size: {
pill: "px-4.5 py-2.5 text-sm font-semibold rounded-full",
md: "px-6 py-2.5 text-xs font-bold rounded-full",
lg: "px-8 py-4.5 text-base font-bold rounded-[20px]",
xl: "px-10 py-5 text-base font-bold rounded-[22px]",
pill: "px-4.5 py-2.5 text-sm font-semibold rounded-2xl",
md: "px-6 py-2.5 text-xs font-bold rounded-2xl",
lg: "px-8 py-4.5 text-base font-bold rounded-2xl",
xl: "px-10 py-5 text-base font-bold rounded-2xl",
},
},
defaultVariants: { variant: "primary", size: "lg" },

View File

@ -0,0 +1,58 @@
type PriceSparklineProps = {
/** 스텝별 가격 (시간순) */
prices: number[]
/** 점선 기준선으로 표시할 목표가 */
targetPrice: number
/** 그려질 비율 0~1 — 진행도에 맞춰 계단이 드러난다 */
progress: number
}
/** 계단식 가격 하락 라인 + 목표가 점선. 진행률만큼 클립으로 드러나는 미니 차트. */
function PriceSparkline({ prices, targetPrice, progress }: PriceSparklineProps) {
const W = 100
const H = 44
// 위아래 여백을 둔 가격 범위 매핑
const hi = Math.max(...prices, targetPrice)
const lo = Math.min(...prices, targetPrice)
const pad = (hi - lo) * 0.08 || 1
const MAX = hi + pad
const MIN = lo - pad
const y = (price: number) => ((MAX - price) / (MAX - MIN)) * H
const stepX = (i: number) => (i / (prices.length - 1)) * W
// step-after 계단 경로
let d = `M 0 ${y(prices[0]).toFixed(1)}`
for (let i = 1; i < prices.length; i++) {
d += ` L ${stepX(i).toFixed(1)} ${y(prices[i - 1]).toFixed(1)} L ${stepX(i).toFixed(1)} ${y(prices[i]).toFixed(1)}`
}
return (
<svg viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="none" className="w-full h-16 overflow-visible" aria-hidden>
<defs>
<clipPath id="price-spark-clip">
<rect x="0" y="-4" width={Math.max(0, Math.min(progress, 1)) * W} height={H + 8} style={{ transition: "width 0.5s ease-out" }} />
</clipPath>
</defs>
{/* 목표가 기준선 */}
<g className="text-primary/40">
<line
x1="0"
y1={y(targetPrice)}
x2={W}
y2={y(targetPrice)}
stroke="currentColor"
strokeWidth="1"
strokeDasharray="4 4"
vectorEffect="non-scaling-stroke"
/>
</g>
{/* 가격 하락 계단 — 진행률만큼 클립으로 드러남 */}
<g clipPath="url(#price-spark-clip)" className="text-primary">
<path d={d} fill="none" stroke="currentColor" strokeWidth="2.5" vectorEffect="non-scaling-stroke" strokeLinejoin="round" />
</g>
</svg>
)
}
export { PriceSparkline }

View File

@ -7,6 +7,7 @@ import { Faq } from "@/components/sections/faq";
import { FinalCTA } from "@/components/sections/final-cta";
import { Footer } from "@/components/sections/footer";
import { Header } from "@/components/sections/header";
import { HeroConsole } from "@/components/sections/hero-console";
import { HeroGlassmorphic } from "@/components/sections/hero-glassmorphic";
import { HeroNeumorphic } from "@/components/sections/hero-neumorphic";
import { HowItWorksDemo } from "@/components/sections/how-it-works-demo";
@ -29,11 +30,11 @@ export function meta() {
];
}
type HeroTheme = "neumorphic" | "glassmorphic";
type HeroTheme = "neumorphic" | "glassmorphic" | "console";
export default function Home() {
// 히어로 디자인 비교용 — 스위처는 dev 에서만 노출, 배포본은 neumorphic(Phone Demo) 고정
const [heroTheme, setHeroTheme] = useState<HeroTheme>("neumorphic");
// 히어로 디자인 비교용 — 스위처는 dev 에서만 노출, 배포본은 console(Live Console) 고정
const [heroTheme, setHeroTheme] = useState<HeroTheme>("console");
return (
<div className="min-h-screen bg-white text-ink font-sans antialiased selection:bg-primary/10 selection:text-primary">
@ -43,6 +44,7 @@ export default function Home() {
{import.meta.env.DEV && <HeroThemeSwitcher value={heroTheme} onChange={setHeroTheme} />}
{heroTheme === "neumorphic" && <HeroNeumorphic />}
{heroTheme === "glassmorphic" && <HeroGlassmorphic />}
{heroTheme === "console" && <HeroConsole />}
</div>
<NegotiationConsole />
@ -61,6 +63,7 @@ export default function Home() {
const HERO_OPTIONS: { value: HeroTheme; label: string }[] = [
{ value: "neumorphic", label: "Phone Demo" },
{ value: "glassmorphic", label: "Soft Ambient" },
{ value: "console", label: "Live Console" },
];
function HeroThemeSwitcher({ value, onChange }: { value: HeroTheme; onChange: (theme: HeroTheme) => void }) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.