[feat] landing: 히어로/데모 리뉴얼 — 콘솔형 히어로·price-sparkline 추가, 데모 미디어(mp4/jpg) 세트 추가, ROI 시뮬레이터·negotiation-console·버튼·홈 라우트 수정
This commit is contained in:
parent
6ffdb9a389
commit
e9427543c8
@ -19,9 +19,9 @@
|
|||||||
--font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
--font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
||||||
|
|
||||||
/* 브랜드 */
|
/* 브랜드 */
|
||||||
--color-primary: #3182F6;
|
--color-primary: #476EFF;
|
||||||
--color-primary-deep: #1B64DA; /* hover */
|
--color-primary-deep: #2F52E0; /* hover */
|
||||||
--color-primary-soft: #E8F3FF; /* 연한 파랑 배경 — 봇 말풍선, 배지 */
|
--color-primary-soft: #ECF0FF; /* 연한 파랑 배경 — 봇 말풍선, 배지 */
|
||||||
|
|
||||||
/* 텍스트. ink 는 다크 섹션에선 배경색으로도 쓴다 */
|
/* 텍스트. ink 는 다크 섹션에선 배경색으로도 쓴다 */
|
||||||
--color-ink: #191F28;
|
--color-ink: #191F28;
|
||||||
@ -38,6 +38,10 @@
|
|||||||
--color-line-strong: #E5E8EB; /* 카드 테두리 */
|
--color-line-strong: #E5E8EB; /* 카드 테두리 */
|
||||||
|
|
||||||
--color-positive: #0B8F57; /* 낙찰·성공 강조 */
|
--color-positive: #0B8F57; /* 낙찰·성공 강조 */
|
||||||
|
--color-positive-soft: #E8F7EF; /* 연한 초록 배경 — 낙찰 배지 */
|
||||||
|
|
||||||
|
--color-accent: #8F52FF; /* 보라 강조 — 결과 탭·히어로 장식 */
|
||||||
|
--color-accent-soft: #F4EFFF; /* 연한 보라 배경 */
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
|
|||||||
51
landing/app/components/sections/hero-console.tsx
Normal file
51
landing/app/components/sections/hero-console.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -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">
|
<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 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 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-sky-300/10 rounded-full filter blur-[100px] 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" />
|
<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
|
<motion.div
|
||||||
animate={{ rotate: -360 }}
|
animate={{ rotate: -360 }}
|
||||||
transition={{ duration: 80, repeat: Infinity, ease: "linear" }}
|
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
|
<motion.div
|
||||||
animate={{ x: [0, 10, 0], y: [0, 10, 0] }}
|
animate={{ x: [0, 10, 0], y: [0, 10, 0] }}
|
||||||
|
|||||||
@ -2,14 +2,13 @@ import { motion } from "motion/react"
|
|||||||
import { ArrowDown, ArrowRight } from "lucide-react"
|
import { ArrowDown, ArrowRight } from "lucide-react"
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { PhoneFrame } from "@/components/ui/device-mockups"
|
|
||||||
import { Typography } from "@/components/ui/typography"
|
import { Typography } from "@/components/ui/typography"
|
||||||
|
|
||||||
/** 대안 히어로 — 뉴모피즘 배경 + 실제 모바일 협상 화면(GIF) 폰 목업. */
|
/** 대안 히어로 — 뉴모피즘 배경 + 실제 모바일 협상 화면(GIF) 폰 목업. */
|
||||||
export function HeroNeumorphic() {
|
export function HeroNeumorphic() {
|
||||||
return (
|
return (
|
||||||
<section className="relative min-h-screen bg-surface-neu text-ink flex flex-col justify-center pt-32 pb-20 overflow-hidden">
|
<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">
|
<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 */}
|
{/* 좌: 카피 + CTA */}
|
||||||
@ -49,7 +48,7 @@ export function HeroNeumorphic() {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 우: 협력사 모바일 협상 실제 화면 (GIF) */}
|
{/* 우: 모바일 협상 채팅 + 외부 설명 UI(제안가·협상카드·낙찰 하이라이트) 합성 영상 */}
|
||||||
<div className="lg:col-span-5 flex justify-center">
|
<div className="lg:col-span-5 flex justify-center">
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, scale: 0.95 }}
|
initial={{ opacity: 0, scale: 0.95 }}
|
||||||
@ -57,13 +56,15 @@ export function HeroNeumorphic() {
|
|||||||
transition={{ duration: 0.8, delay: 0.2 }}
|
transition={{ duration: 0.8, delay: 0.2 }}
|
||||||
className="w-full flex justify-center"
|
className="w-full flex justify-center"
|
||||||
>
|
>
|
||||||
<PhoneFrame>
|
<video
|
||||||
<img
|
src="/gifs/negotiation_annotated.mp4"
|
||||||
src="/gifs/negotiation.gif"
|
poster="/gifs/negotiation_annotated.jpg"
|
||||||
alt="협력사 모바일 협상 포털 실제 화면"
|
autoPlay
|
||||||
className="w-full h-full object-cover"
|
muted
|
||||||
/>
|
loop
|
||||||
</PhoneFrame>
|
playsInline
|
||||||
|
className="w-full max-w-[440px] h-auto"
|
||||||
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -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 (
|
return (
|
||||||
<img
|
<img
|
||||||
src={tab.gif}
|
src={tab.gif}
|
||||||
@ -151,6 +167,8 @@ type DemoTab = {
|
|||||||
mockup: "browser" | "phone"
|
mockup: "browser" | "phone"
|
||||||
url?: string
|
url?: string
|
||||||
gif: string
|
gif: string
|
||||||
|
video?: string
|
||||||
|
poster?: string
|
||||||
gifAlt: string
|
gifAlt: string
|
||||||
fallback: {
|
fallback: {
|
||||||
icon: LucideIcon
|
icon: LucideIcon
|
||||||
@ -188,6 +206,8 @@ const DEMO_TABS: DemoTab[] = [
|
|||||||
mockup: "browser",
|
mockup: "browser",
|
||||||
url: "https://console.negotium.ai/estimates/new",
|
url: "https://console.negotium.ai/estimates/new",
|
||||||
gif: "/gifs/quote_generation.gif",
|
gif: "/gifs/quote_generation.gif",
|
||||||
|
video: "/gifs/quote_generation.mp4",
|
||||||
|
poster: "/gifs/quote_generation.jpg",
|
||||||
gifAlt: "견적 생성 가이드라인 수립 시연",
|
gifAlt: "견적 생성 가이드라인 수립 시연",
|
||||||
fallback: {
|
fallback: {
|
||||||
icon: Monitor,
|
icon: Monitor,
|
||||||
@ -201,8 +221,8 @@ const DEMO_TABS: DemoTab[] = [
|
|||||||
key: "negotiate",
|
key: "negotiate",
|
||||||
tabIcon: Smartphone,
|
tabIcon: Smartphone,
|
||||||
tabLabel: "2단계: AI 자동 밀당 협상",
|
tabLabel: "2단계: AI 자동 밀당 협상",
|
||||||
badge: { icon: Bot, label: "협력사 협상 포털 (웹·모바일)", className: "bg-emerald-50 border border-emerald-100 text-positive" },
|
badge: { icon: Bot, label: "협력사 협상 포털 (웹·모바일)", className: "bg-positive-soft border border-positive/15 text-positive" },
|
||||||
checkClassName: "text-emerald-500",
|
checkClassName: "text-positive",
|
||||||
heading: (
|
heading: (
|
||||||
<>
|
<>
|
||||||
공급사는 설치 없이 <br />
|
공급사는 설치 없이 <br />
|
||||||
@ -224,7 +244,7 @@ const DEMO_TABS: DemoTab[] = [
|
|||||||
gifAlt: "AI 모바일 자동 협상 시연",
|
gifAlt: "AI 모바일 자동 협상 시연",
|
||||||
fallback: {
|
fallback: {
|
||||||
icon: Smartphone,
|
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",
|
codeClassName: "bg-fill text-positive",
|
||||||
title: "협상 시연 GIF 공간",
|
title: "협상 시연 GIF 공간",
|
||||||
hint: "실제 모바일 협상 시뮬레이션 GIF를 배치해 주세요.",
|
hint: "실제 모바일 협상 시뮬레이션 GIF를 배치해 주세요.",
|
||||||
@ -234,8 +254,8 @@ const DEMO_TABS: DemoTab[] = [
|
|||||||
key: "result",
|
key: "result",
|
||||||
tabIcon: Monitor,
|
tabIcon: Monitor,
|
||||||
tabLabel: "3단계: AI 협상 결과 확인",
|
tabLabel: "3단계: AI 협상 결과 확인",
|
||||||
badge: { icon: Monitor, label: "구매 관리자 콘솔 (웹)", className: "bg-purple-50 border border-purple-100 text-purple-600" },
|
badge: { icon: Monitor, label: "구매 관리자 콘솔 (웹)", className: "bg-accent-soft border border-accent/15 text-accent" },
|
||||||
checkClassName: "text-purple-500",
|
checkClassName: "text-accent",
|
||||||
heading: (
|
heading: (
|
||||||
<>
|
<>
|
||||||
자동 분석 및 타결 보고서 <br />
|
자동 분석 및 타결 보고서 <br />
|
||||||
@ -255,11 +275,13 @@ const DEMO_TABS: DemoTab[] = [
|
|||||||
mockup: "browser",
|
mockup: "browser",
|
||||||
url: "https://console.negotium.ai/dashboard/reports",
|
url: "https://console.negotium.ai/dashboard/reports",
|
||||||
gif: "/gifs/negotiation_result.gif",
|
gif: "/gifs/negotiation_result.gif",
|
||||||
|
video: "/gifs/negotiation_result.mp4",
|
||||||
|
poster: "/gifs/negotiation_result.jpg",
|
||||||
gifAlt: "협상 결과 분석 대시보드 시연",
|
gifAlt: "협상 결과 분석 대시보드 시연",
|
||||||
fallback: {
|
fallback: {
|
||||||
icon: Monitor,
|
icon: Monitor,
|
||||||
iconWrapClassName: "w-14 h-14 bg-purple-50 rounded-2xl text-purple-600",
|
iconWrapClassName: "w-14 h-14 bg-accent-soft rounded-2xl text-accent",
|
||||||
codeClassName: "bg-white text-purple-600",
|
codeClassName: "bg-white text-accent",
|
||||||
title: "협상 결과 확인 시연 GIF 공간",
|
title: "협상 결과 확인 시연 GIF 공간",
|
||||||
hint: "결과 보고 대시보드 기동 GIF를 배치하시면 이 영역에 자동으로 재생됩니다.",
|
hint: "결과 보고 대시보드 기동 GIF를 배치하시면 이 영역에 자동으로 재생됩니다.",
|
||||||
},
|
},
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from "react"
|
|||||||
import { AnimatePresence, motion } from "motion/react"
|
import { AnimatePresence, motion } from "motion/react"
|
||||||
import { Bot, Handshake, ShieldCheck } from "lucide-react"
|
import { Bot, Handshake, ShieldCheck } from "lucide-react"
|
||||||
|
|
||||||
|
import { PriceSparkline } from "@/components/ui/price-sparkline"
|
||||||
import { SlateRenderer } from "@/components/ui/slate-renderer"
|
import { SlateRenderer } from "@/components/ui/slate-renderer"
|
||||||
import { Typography } from "@/components/ui/typography"
|
import { Typography } from "@/components/ui/typography"
|
||||||
import { EASE_OUT_EXPO } from "@/lib/motion"
|
import { EASE_OUT_EXPO } from "@/lib/motion"
|
||||||
@ -105,7 +106,11 @@ export function NegotiationConsole() {
|
|||||||
|
|
||||||
{/* 계단식 하락 스파크라인 + 목표가 기준선 */}
|
{/* 계단식 하락 스파크라인 + 목표가 기준선 */}
|
||||||
<div>
|
<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">
|
<div className="flex justify-between mt-2">
|
||||||
<Typography variant="micro">시작가 {START_PRICE.toLocaleString()}</Typography>
|
<Typography variant="micro">시작가 {START_PRICE.toLocaleString()}</Typography>
|
||||||
<Typography variant="micro" className="text-primary">
|
<Typography variant="micro" className="text-primary">
|
||||||
@ -125,7 +130,7 @@ export function NegotiationConsole() {
|
|||||||
initial={{ scale: 0.95, opacity: 0 }}
|
initial={{ scale: 0.95, opacity: 0 }}
|
||||||
animate={{ scale: 1, opacity: 1 }}
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
exit={{ scale: 0.95, opacity: 0 }}
|
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" />
|
<ShieldCheck className="w-4 h-4 shrink-0" />
|
||||||
{NEGOTIATION_STEPS[activeStep].badge}
|
{NEGOTIATION_STEPS[activeStep].badge}
|
||||||
@ -220,54 +225,6 @@ export function NegotiationConsole() {
|
|||||||
const START_PRICE = 1200000
|
const START_PRICE = 1200000
|
||||||
const TARGET_PRICE = 1080000
|
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스텝
|
// 데모 대화 시나리오 — 1,200,000원 제시에서 1,050,000원 낙찰까지 6스텝
|
||||||
const NEGOTIATION_STEPS: DialogueStep[] = [
|
const NEGOTIATION_STEPS: DialogueStep[] = [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -77,7 +77,7 @@ export function ROISimulator() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-3 bg-surface p-5 rounded-2xl border border-line">
|
<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">
|
<Typography variant="caption" className="text-ink-soft">
|
||||||
본 시뮬레이션은 글로벌 최저가 스캔(LPS) 시장 표준 및 다수 B2B 자율 협상 데이터의 보수적인 평균 수치(3.1% ~ 4.2%
|
본 시뮬레이션은 글로벌 최저가 스캔(LPS) 시장 표준 및 다수 B2B 자율 협상 데이터의 보수적인 평균 수치(3.1% ~ 4.2%
|
||||||
절감)를 적용하여 산정되었습니다. 실사 수준의 검증은 파일 연동을 통해 무상 지원됩니다.
|
절감)를 적용하여 산정되었습니다. 실사 수준의 검증은 파일 연동을 통해 무상 지원됩니다.
|
||||||
|
|||||||
@ -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",
|
glass: "bg-white/70 hover:bg-white text-ink-soft hover:text-ink border border-white shadow-sm",
|
||||||
},
|
},
|
||||||
|
// radius 는 사이즈 무관 rounded-2xl(16px) 로 통일 — 알약형 금지
|
||||||
size: {
|
size: {
|
||||||
pill: "px-4.5 py-2.5 text-sm font-semibold rounded-full",
|
pill: "px-4.5 py-2.5 text-sm font-semibold rounded-2xl",
|
||||||
md: "px-6 py-2.5 text-xs font-bold rounded-full",
|
md: "px-6 py-2.5 text-xs font-bold rounded-2xl",
|
||||||
lg: "px-8 py-4.5 text-base font-bold rounded-[20px]",
|
lg: "px-8 py-4.5 text-base font-bold rounded-2xl",
|
||||||
xl: "px-10 py-5 text-base font-bold rounded-[22px]",
|
xl: "px-10 py-5 text-base font-bold rounded-2xl",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: { variant: "primary", size: "lg" },
|
defaultVariants: { variant: "primary", size: "lg" },
|
||||||
|
|||||||
58
landing/app/components/ui/price-sparkline.tsx
Normal file
58
landing/app/components/ui/price-sparkline.tsx
Normal 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 }
|
||||||
@ -7,6 +7,7 @@ import { Faq } from "@/components/sections/faq";
|
|||||||
import { FinalCTA } from "@/components/sections/final-cta";
|
import { FinalCTA } from "@/components/sections/final-cta";
|
||||||
import { Footer } from "@/components/sections/footer";
|
import { Footer } from "@/components/sections/footer";
|
||||||
import { Header } from "@/components/sections/header";
|
import { Header } from "@/components/sections/header";
|
||||||
|
import { HeroConsole } from "@/components/sections/hero-console";
|
||||||
import { HeroGlassmorphic } from "@/components/sections/hero-glassmorphic";
|
import { HeroGlassmorphic } from "@/components/sections/hero-glassmorphic";
|
||||||
import { HeroNeumorphic } from "@/components/sections/hero-neumorphic";
|
import { HeroNeumorphic } from "@/components/sections/hero-neumorphic";
|
||||||
import { HowItWorksDemo } from "@/components/sections/how-it-works-demo";
|
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() {
|
export default function Home() {
|
||||||
// 히어로 디자인 비교용 — 스위처는 dev 에서만 노출, 배포본은 neumorphic(Phone Demo) 고정
|
// 히어로 디자인 비교용 — 스위처는 dev 에서만 노출, 배포본은 console(Live Console) 고정
|
||||||
const [heroTheme, setHeroTheme] = useState<HeroTheme>("neumorphic");
|
const [heroTheme, setHeroTheme] = useState<HeroTheme>("console");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white text-ink font-sans antialiased selection:bg-primary/10 selection:text-primary">
|
<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} />}
|
{import.meta.env.DEV && <HeroThemeSwitcher value={heroTheme} onChange={setHeroTheme} />}
|
||||||
{heroTheme === "neumorphic" && <HeroNeumorphic />}
|
{heroTheme === "neumorphic" && <HeroNeumorphic />}
|
||||||
{heroTheme === "glassmorphic" && <HeroGlassmorphic />}
|
{heroTheme === "glassmorphic" && <HeroGlassmorphic />}
|
||||||
|
{heroTheme === "console" && <HeroConsole />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<NegotiationConsole />
|
<NegotiationConsole />
|
||||||
@ -61,6 +63,7 @@ export default function Home() {
|
|||||||
const HERO_OPTIONS: { value: HeroTheme; label: string }[] = [
|
const HERO_OPTIONS: { value: HeroTheme; label: string }[] = [
|
||||||
{ value: "neumorphic", label: "Phone Demo" },
|
{ value: "neumorphic", label: "Phone Demo" },
|
||||||
{ value: "glassmorphic", label: "Soft Ambient" },
|
{ value: "glassmorphic", label: "Soft Ambient" },
|
||||||
|
{ value: "console", label: "Live Console" },
|
||||||
];
|
];
|
||||||
|
|
||||||
function HeroThemeSwitcher({ value, onChange }: { value: HeroTheme; onChange: (theme: HeroTheme) => void }) {
|
function HeroThemeSwitcher({ value, onChange }: { value: HeroTheme; onChange: (theme: HeroTheme) => void }) {
|
||||||
|
|||||||
BIN
landing/public/gifs/hero_console.jpg
Normal file
BIN
landing/public/gifs/hero_console.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 111 KiB |
BIN
landing/public/gifs/hero_console.mp4
Normal file
BIN
landing/public/gifs/hero_console.mp4
Normal file
Binary file not shown.
BIN
landing/public/gifs/negotiation.jpg
Normal file
BIN
landing/public/gifs/negotiation.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
BIN
landing/public/gifs/negotiation.mp4
Normal file
BIN
landing/public/gifs/negotiation.mp4
Normal file
Binary file not shown.
BIN
landing/public/gifs/negotiation_annotated.jpg
Normal file
BIN
landing/public/gifs/negotiation_annotated.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 99 KiB |
BIN
landing/public/gifs/negotiation_annotated.mp4
Normal file
BIN
landing/public/gifs/negotiation_annotated.mp4
Normal file
Binary file not shown.
BIN
landing/public/gifs/negotiation_result.jpg
Normal file
BIN
landing/public/gifs/negotiation_result.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
BIN
landing/public/gifs/negotiation_result.mp4
Normal file
BIN
landing/public/gifs/negotiation_result.mp4
Normal file
Binary file not shown.
BIN
landing/public/gifs/quote_generation.jpg
Normal file
BIN
landing/public/gifs/quote_generation.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
BIN
landing/public/gifs/quote_generation.mp4
Normal file
BIN
landing/public/gifs/quote_generation.mp4
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user