[feat] landing: 카피 정직화(가이드 3탭·강화학습·이점)·실단가 반영, 비교 토글·FAQ 섹션 신설, 협상 콘솔 목표가 스파크라인·절감 델타 추가, 헤더 플리커·히어로 줄바꿈 수정

This commit is contained in:
Mina Choi 2026-07-21 10:47:45 +09:00
parent e93820a51b
commit 9ccf42cf6b
11 changed files with 435 additions and 80 deletions

View File

@ -0,0 +1,166 @@
import { useEffect, useRef, useState } from "react"
import { AnimatePresence, motion } from "motion/react"
import { Bot, Clock, Database, Handshake, Scale, TrendingUp, UserRound, type LucideIcon } from "lucide-react"
import { Section } from "@/components/ui/section"
import { SectionHeading } from "@/components/ui/section-heading"
import { Typography } from "@/components/ui/typography"
import { useFadeUp } from "@/lib/motion"
/** 기존 방식 ↔ 도입 후 토글 비교 — 화면 진입 시 자동으로 오가고, 사용자가 누르면 수동 고정. */
export function Comparison() {
const fadeUp = useFadeUp()
const toggleFadeUp = useFadeUp(0.1)
const [mode, setMode] = useState<Mode>("before")
// 사용자가 직접 토글을 누르면 자동 전환 중단
const [locked, setLocked] = useState(false)
const [inView, setInView] = useState(false)
const sectionRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const el = sectionRef.current
if (!el) return
const observer = new IntersectionObserver(([entry]) => setInView(entry.isIntersecting), { threshold: 0.35 })
observer.observe(el)
return () => observer.disconnect()
}, [])
useEffect(() => {
if (!inView || locked) return
const timer = setInterval(() => setMode((m) => (m === "before" ? "after" : "before")), 3000)
return () => clearInterval(timer)
}, [inView, locked])
return (
<Section id="comparison" bordered>
<motion.div {...fadeUp} className="mb-12">
<SectionHeading
align="center"
eyebrow="WHY NEGOTIUM"
title="협상 방식을 바꾸면, 무엇이 달라질까요"
description="기존 가격 협상의 문제는 사람이 아니라 구조입니다. 버튼을 눌러 직접 비교해 보세요."
descriptionClassName="mt-4 text-[17px] max-w-2xl"
/>
</motion.div>
{/* 모드 토글 — 이용 가이드 탭과 같은 세그먼트 문법 */}
<motion.div {...toggleFadeUp} className="flex justify-center mb-14">
<div className="p-1 bg-fill rounded-[14px] inline-flex gap-1">
{MODES.map((m) => (
<button
key={m.key}
onClick={() => {
setLocked(true)
setMode(m.key)
}}
className={`px-5 py-2.5 rounded-[10px] text-xs sm:text-sm font-bold transition-all flex items-center gap-2 cursor-pointer ${
mode === m.key ? "bg-white text-primary shadow-sm" : "text-ink-soft hover:text-ink"
}`}
>
{m.key === "after" && <Bot className="w-4 h-4" />}
<span>{m.label}</span>
</button>
))}
</div>
</motion.div>
<div ref={sectionRef} className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{ITEMS.map((item, idx) => (
<CompareCard key={item.label} item={item} mode={mode} index={idx} />
))}
</div>
</Section>
)
}
type Mode = "before" | "after"
const MODES: { key: Mode; label: string }[] = [
{ key: "before", label: "기존 방식" },
{ key: "after", label: "negotium 도입 후" },
]
type CompareItem = { label: string; icon: LucideIcon; before: string; after: string }
function CompareCard({ item, mode, index }: { item: CompareItem; mode: Mode; index: number }) {
const fadeUp = useFadeUp(0.06 * (index + 1))
const active = mode === "after"
return (
<motion.div
{...fadeUp}
className={`p-7 rounded-[28px] flex flex-col gap-5 transition-colors duration-500 ${
active ? "bg-primary-soft/60" : "bg-surface"
}`}
>
<div className="flex items-center justify-between">
<div
className={`w-11 h-11 rounded-2xl flex items-center justify-center transition-colors duration-500 ${
active ? "bg-primary/10 text-primary" : "bg-ink-muted/10 text-ink-muted"
}`}
>
<item.icon className="w-5 h-5" />
</div>
<Typography variant="micro" className={active ? "text-primary" : ""}>
{item.label}
</Typography>
</div>
{/* 전환 시 레이아웃 점프 방지용 최소 높이 */}
<div className="min-h-[72px]">
<AnimatePresence mode="wait">
<motion.div
key={mode}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -8 }}
transition={{ duration: 0.28, delay: 0.05 * index }}
>
<Typography variant="small" className={active ? "text-ink font-semibold" : "text-ink-muted"}>
{active ? item.after : item.before}
</Typography>
</motion.div>
</AnimatePresence>
</div>
</motion.div>
)
}
const ITEMS: CompareItem[] = [
{
label: "거래 시간",
icon: Clock,
before: "일일 거래의 한계 — 사람의 인력과 시간만큼만 협상합니다.",
after: "24시간 연중무휴, 전 품목·전 공급사와 동시에 협상합니다.",
},
{
label: "협상 성과",
icon: TrendingUp,
before: "담당자의 역량·감정·컨디션에 따라 성과가 흔들립니다.",
after: "전략 강화 학습으로 기준선이 상승하고, 점진적으로 개선됩니다.",
},
{
label: "노하우",
icon: Database,
before: "잘 깎는 노하우가 개인의 머릿속에만 남습니다.",
after: "모든 협상이 데이터로 축적되어 다음 전략에 재활용됩니다.",
},
{
label: "사람",
icon: UserRound,
before: "반복 흥정과 감정 노동에 지쳐 고급 인력이 이탈합니다.",
after: "감정 소모는 봇이 맡고, 사람은 전략 업무와 대형 거래에 집중합니다.",
},
{
label: "파트너 관계",
icon: Handshake,
before: "감정 과열과 실랑이로 장기 파트너십이 흔들립니다.",
after: "악역은 봇이 — 압박 없는 상시 협상으로 관계가 개선됩니다.",
},
{
label: "의사 결정",
icon: Scale,
before: "개인 편향과 컴플라이언스 리스크에 노출됩니다.",
after: "객관적·데이터 기반의 투명한 프로세스로 기록됩니다.",
},
]

View File

@ -78,7 +78,7 @@ export function Contact() {
type="text"
name="companyName"
required
placeholder="예: 주식회사 네고시움"
placeholder="예: (주)한빛유통"
value={formData.companyName}
onChange={handleChange}
disabled={submitting}

View File

@ -17,7 +17,7 @@ export function CoreValues() {
align="center"
eyebrow="BEYOND EXPECTED VALUE"
title="가격 그 이상의 이점"
description="단순 업무 긴축을 넘어선 진정한 구매 거버넌스 혁신"
description="절감액은 시작일 뿐 — 관계, 투명성, 그리고 사람의 시간까지 지킵니다."
descriptionClassName="mt-4 text-[17px]"
/>
</motion.div>
@ -56,24 +56,24 @@ const VALUES: Value[] = [
icon: Users,
title: "파트너사 관계 수호",
description:
"단가를 조율하는 과정에서 수반되는 피로감과 실랑이는 온전히 봇이 감당합니다. 담당 부서는 협력사 담당자와 신뢰할 수 있는 인간적 유대를 단단히 하고 동반 성장을 도모하는 큰 틀의 파트너십에만 집중할 수 있습니다.",
"단가를 깎는 악역과 감정 소모는 봇이 맡습니다. 담당자는 협력사와의 신뢰와 동반 성장, 큰 틀의 파트너십에만 집중하세요.",
},
{
icon: Lock,
title: "완벽하고 투명한 거버넌스",
title: "투명한 거버넌스",
description:
"모든 제안 접수와 흥정 로그, 조율 타임라인이 위변조가 차단된 고유 원장에 실시간 적재됩니다. 감사 대응이나 의사결정 추적 분석 시 완벽에 가까운 거버넌스를 보장합니다.",
"모든 제안과 협상 로그, 조율 타임라인이 한 줄도 빠짐없이 기록됩니다. 감사 대응도, 의사결정 추적도 클릭 한 번이면 됩니다.",
},
{
icon: Briefcase,
title: "핵심 전략에만 집중",
description:
"수많은 이메일 교환, 메신저 단가 실랑이 등 정형적이고 반복적인 흥정 수작업에서 구매 부서를 구해냅니다. 공급망 위기 대처, 우량 소스 발굴과 같은 진정 가치 있는 혁신적 과업에 인재들을 투입하세요.",
"이메일·메신저로 반복되던 흥정 수작업은 봇이 전담합니다. 인력은 공급망 위기 대처, 우량 공급처 발굴 같은 전략 업무와 대형 거래에 투입하세요.",
},
{
icon: Clock,
title: "협상 리드타임 대폭 긴축",
title: "협상 리드타임 축",
description:
"수백 공급사와 동시에 병렬 조율을 자동으로 전개하므로 업무량의 한계가 없습니다. 통상 수주일 동안 지루하게 이어지던 최종 가격 타결 주기를 단 수일 안으로 긴축 단축시킬 수 있습니다.",
"수백 공급사와 동시에 협상하므로 사람 수에 얽매이지 않습니다. 수주씩 걸리던 가격 타결을 수일 안에 끝냅니다.",
},
]

View File

@ -0,0 +1,112 @@
import { useState } from "react"
import { AnimatePresence, motion } from "motion/react"
import { ChevronDown } from "lucide-react"
import { Section } from "@/components/ui/section"
import { SectionHeading } from "@/components/ui/section-heading"
import { Typography } from "@/components/ui/typography"
import { EASE_OUT_EXPO, useFadeUp } from "@/lib/motion"
/** 도입 검토 FAQ 아코디언 — 첫 항목 기본 열림, 한 번에 하나만 펼침. */
export function Faq() {
const fadeUp = useFadeUp()
const [openIndex, setOpenIndex] = useState<number | null>(0)
return (
<Section id="faq" width="sm" bordered>
<motion.div {...fadeUp} className="mb-24">
<SectionHeading
align="center"
eyebrow="FAQ"
title="자주 묻는 질문"
description="도입 검토에서 가장 많이 받는 질문들입니다."
descriptionClassName="mt-4 text-[17px]"
/>
</motion.div>
<div className="space-y-4">
{FAQS.map((faq, idx) => (
<FaqItem
key={faq.question}
faq={faq}
open={openIndex === idx}
onToggle={() => setOpenIndex(openIndex === idx ? null : idx)}
delay={0.1 * (idx + 1)}
/>
))}
</div>
</Section>
)
}
type FaqEntry = { question: string; answer: string }
function FaqItem({
faq,
open,
onToggle,
delay,
}: {
faq: FaqEntry
open: boolean
onToggle: () => void
delay: number
}) {
const fadeUp = useFadeUp(delay)
return (
<motion.div {...fadeUp} className="bg-surface rounded-[28px] overflow-hidden">
<button
type="button"
onClick={onToggle}
aria-expanded={open}
className="w-full flex items-center justify-between gap-6 text-left px-8 py-7 cursor-pointer"
>
<Typography variant="cardTitle" as="span">
{faq.question}
</Typography>
<motion.span
animate={{ rotate: open ? 180 : 0 }}
transition={{ duration: 0.4, ease: EASE_OUT_EXPO }}
className="text-ink-muted shrink-0"
aria-hidden
>
<ChevronDown className="w-5 h-5" />
</motion.span>
</button>
<AnimatePresence initial={false}>
{open && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.5, ease: EASE_OUT_EXPO }}
className="overflow-hidden"
>
<Typography variant="small" className="px-8 pb-8">
{faq.answer}
</Typography>
</motion.div>
)}
</AnimatePresence>
</motion.div>
)
}
const FAQS: FaqEntry[] = [
{
question: "공급사가 봇과의 협상을 싫어하지 않을까요?",
answer:
"오히려 반대입니다. 이 거래들 대부분은 지금껏 협상 테이블에 오르지도 못하던 건입니다. 봇은 24시간 원하는 시간에, 압박 없이, 늘 같은 기준으로 응대합니다. 글로벌 동종 서비스의 공급사 만족도는 82%에 이릅니다.",
},
{
question: "봇이 우리 기준을 벗어나 합의해 버리면요?",
answer:
"그럴 수 없습니다. 봇은 견적을 만들 때 정한 목표가와 낙찰 기준 밖으로 나가지 않고, 최종 낙찰 규칙도 사용자가 정합니다.",
},
{
question: "기존 시스템과 연동되나요?",
answer: "견적·발주 데이터 연동을 지원합니다. 도입 상담에서 사내 ERP 환경에 맞는 연동 방식을 안내해 드립니다.",
},
]

View File

@ -18,7 +18,7 @@ export function Header() {
return (
<header
className={`fixed top-0 left-0 right-0 z-40 transition-all duration-300 ${
scrolled ? "py-4 bg-white/80 backdrop-blur-xl border-b border-line" : "py-6 bg-transparent"
scrolled ? "py-4 bg-white/80 backdrop-blur-xl border-b border-line" : "py-6 bg-transparent border-b border-transparent"
}`}
>
<div className="max-w-5xl mx-auto px-6 flex items-center justify-between">
@ -48,5 +48,5 @@ const NAV_LINKS = [
{ href: "#how-it-works-demo", label: "이용 가이드" },
{ href: "#core-values", label: "솔루션 이점" },
{ href: "#reinforcement", label: "강화학습" },
{ href: "#benchmarks", label: "보안 및 신뢰성" },
{ href: "#faq", label: "자주 묻는 질문" },
]

View File

@ -45,7 +45,7 @@ export function HeroGlassmorphic() {
transition={{ duration: 0.9, delay: 0.1, ease: EASE_OUT_EXPO }}
>
<Typography variant="display" className="mb-8">
, <span className="text-primary"> </span> <br />
, <span className="text-primary"> </span> <br className="hidden sm:inline" />
</Typography>
</motion.div>
@ -57,7 +57,7 @@ export function HeroGlassmorphic() {
>
<Typography variant="lead" className="md:text-[20px] max-w-2xl mb-12">
. <br className="hidden sm:inline" />
() , .
, , 24 .
</Typography>
</motion.div>

View File

@ -1,6 +1,7 @@
import { motion } from "motion/react"
import { ArrowDown, ArrowRight, Sparkles } from "lucide-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"
@ -13,19 +14,10 @@ export function HeroNeumorphic() {
<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 */}
<div className="lg:col-span-7 space-y-8 text-left">
<motion.div
initial={{ opacity: 0, y: 15 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7 }}
className="inline-flex items-center gap-2 px-3.5 py-1.5 bg-surface-neu rounded-full text-xs font-bold text-primary shadow-[3px_3px_6px_#dbdee3,-3px_-3px_6px_#ffffff]"
>
<Sparkles className="w-3.5 h-3.5" />
<span>LIVE NEGOTIATION DEMO</span>
</motion.div>
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.8, delay: 0.1 }}>
<Typography variant="display" className="text-4xl sm:text-5xl lg:text-[54px] leading-[1.15]">
, <span className="text-primary"> </span> <br />
, <br />
<span className="text-primary"> </span> <br />
</Typography>
</motion.div>
@ -43,20 +35,17 @@ export function HeroNeumorphic() {
transition={{ duration: 0.8, delay: 0.3 }}
className="flex flex-col sm:flex-row gap-5 pt-4"
>
<a
<Button
href="#contact-section"
className="px-8 py-4.5 bg-primary hover:bg-primary-deep text-white text-base font-bold rounded-[20px] cursor-pointer flex items-center justify-center gap-2 group transition-all shadow-[6px_6px_12px_rgba(49,130,246,0.25),-6px_-6px_12px_#ffffff] hover:shadow-[4px_4px_8px_rgba(49,130,246,0.25),-4px_-4px_8px_#ffffff] active:shadow-none translate-y-0 active:translate-y-0.5"
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" />
</a>
<a
href="#how-it-works"
className="px-8 py-4.5 bg-surface-neu text-ink-soft hover:text-ink text-base font-bold rounded-[20px] transition-all flex items-center justify-center gap-1.5 shadow-[6px_6px_12px_#dbdee3,-6px_-6px_12px_#ffffff] hover:shadow-[4px_4px_8px_#dbdee3,-4px_-4px_8px_#ffffff] active:shadow-[inset_3px_3px_6px_#dbdee3,inset_-3px_-3px_6px_#ffffff] translate-y-0 active:translate-y-0.5"
>
</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" />
</a>
</Button>
</motion.div>
</div>

View File

@ -19,7 +19,7 @@ export function HowItWorksDemo() {
align="center"
className="mb-20"
eyebrow="SERVICE DEMONSTRATION"
title="견적 생성부터 AI 협상까지 한눈에 보기"
title="견적 생성부터 낙찰까지 한눈에 보기"
description="복잡해 보이는 구매 과정이 어떻게 자동화되는지 실제 작동 화면(GIF)을 통해 쉽고 직관적으로 확인해 보세요."
descriptionClassName="text-[17px] max-w-2xl"
/>
@ -176,15 +176,15 @@ const DEMO_TABS: DemoTab[] = [
),
paragraphs: [
<>
, negotium <b>LPS(Lowest Price Scanning)</b>
.
, negotium<b> (LPS)</b> {" "}
<b className="text-primary"> .</b>
</>,
<>
, {" "}
<b className="text-primary"> (Target Price)</b> .
, {" "}
<b className="text-primary"> (Target Price)</b> .
</>,
],
checks: ["원자재 및 공정 시장 실시간 시세 연동", "이탈 저항 임계 모델 기반 가이드 자동 완성"],
checks: ["인터넷 최저가(LPS) 시세 상시 반영", "마진을 지키는 최적 목표 단가 자동 완성"],
mockup: "browser",
url: "https://console.negotium.ai/estimates/new",
gif: "/gifs/quote_generation.gif",
@ -205,13 +205,13 @@ const DEMO_TABS: DemoTab[] = [
checkClassName: "text-emerald-500",
heading: (
<>
<br />
<br />
</>
),
paragraphs: [
<>
<b> </b> .
<b> </b> , .
</>,
<>
, <b>"물량 보증"</b> <b>"지불 주기 단축"</b>
@ -244,15 +244,14 @@ const DEMO_TABS: DemoTab[] = [
),
paragraphs: [
<>
, AI가
.
, <b> </b> .
</>,
<>
( , )
.
<b> </b>,
.
</>,
],
checks: ["최종 단가 조율 결과 및 계약 체결 자동 확정", "총 누적 절감액(Savings) 및 마진 방어 통계 실시간 표기"],
checks: ["낙찰 결과·절감액 자동 집계", "누적 절감액·협상 통계 대시보드"],
mockup: "browser",
url: "https://console.negotium.ai/dashboard/reports",
gif: "/gifs/negotiation_result.gif",

View File

@ -3,6 +3,7 @@ import { AnimatePresence, motion } from "motion/react"
import { Bot, Handshake, ShieldCheck } from "lucide-react"
import { SlateRenderer } from "@/components/ui/slate-renderer"
import { Typography } from "@/components/ui/typography"
import { EASE_OUT_EXPO } from "@/lib/motion"
import type { DialogueStep } from "@/types"
@ -10,7 +11,7 @@ import type { DialogueStep } from "@/types"
export function NegotiationConsole() {
// -1 = 아직 섹션 진입 전(온보딩 가이드 표시)
const [activeStep, setActiveStep] = useState(-1)
const [animatedPrice, setAnimatedPrice] = useState(1200)
const [animatedPrice, setAnimatedPrice] = useState(1200000)
const sectionRef = useRef<HTMLDivElement>(null)
const chatContainerRef = useRef<HTMLDivElement>(null)
@ -39,7 +40,7 @@ export function NegotiationConsole() {
// 단가 오도미터 애니메이션 (500ms ease-out)
useEffect(() => {
const targetPrice = activeStep === -1 ? 1200 : NEGOTIATION_STEPS[activeStep].price
const targetPrice = activeStep === -1 ? 1200000 : NEGOTIATION_STEPS[activeStep].price
if (animatedPrice === targetPrice) return
const start = animatedPrice
@ -68,6 +69,16 @@ export function NegotiationConsole() {
className="relative bg-surface text-ink w-full font-sans border-t border-line h-[240vh] md:h-[280vh]"
>
<div className="sticky top-0 h-screen flex items-center justify-center w-full overflow-hidden px-6 md:px-8">
{/* 섹션 헤딩 — sticky 상단 고정 (모바일은 공간상 생략) */}
<div className="absolute top-24 left-0 right-0 text-center hidden md:block">
<Typography variant="eyebrow" className="block">
Live Replay
</Typography>
<Typography variant="heading" as="h2" className="mt-1">
,
</Typography>
</div>
<div className="max-w-4xl mx-auto grid grid-cols-1 md:grid-cols-12 gap-12 md:gap-16 items-center w-full">
{/* 좌: 실시간 단가 패널 */}
<div className="md:col-span-5 flex flex-col justify-center space-y-6">
@ -75,10 +86,35 @@ export function NegotiationConsole() {
<span className="text-[11px] font-bold text-ink-muted uppercase tracking-wider block mb-1"> </span>
<div className="flex items-baseline gap-1.5">
<span className="text-3xl font-bold text-primary"></span>
<span className="text-5xl md:text-6xl font-black tracking-tight text-primary">
<span className="text-4xl md:text-5xl font-black tracking-tight text-primary">
{animatedPrice.toLocaleString()}
</span>
</div>
{/* 시작가 대비 누적 절감 델타 — 높이 고정으로 레이아웃 점프 방지 */}
<div className="h-6 mt-1.5">
{animatedPrice < START_PRICE && (
<span className="inline-flex items-baseline gap-1.5 text-positive font-bold text-sm">
{(START_PRICE - animatedPrice).toLocaleString()}
<span className="text-xs font-semibold">
({(((START_PRICE - animatedPrice) / START_PRICE) * 100).toFixed(1)}%)
</span>
</span>
)}
</div>
</div>
{/* 계단식 하락 스파크라인 + 목표가 기준선 */}
<div>
<PriceSparkline activeStep={activeStep} />
<div className="flex justify-between mt-2">
<Typography variant="micro"> {START_PRICE.toLocaleString()}</Typography>
<Typography variant="micro" className="text-primary">
{TARGET_PRICE.toLocaleString()}
</Typography>
</div>
<Typography variant="caption" className="mt-3">
.
</Typography>
</div>
<div className="h-10">
@ -99,7 +135,7 @@ export function NegotiationConsole() {
</div>
</div>
{/* 우: 대화 말풍선 (파트너 좌 / 봇 우) */}
{/* 우: 대화 말풍선 (봇 좌 / 파트너 우) */}
<div className="md:col-span-7 flex flex-col justify-end space-y-5 h-[340px] md:h-[420px] relative overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-b from-surface via-transparent to-transparent h-12 pointer-events-none z-10" />
@ -141,9 +177,9 @@ export function NegotiationConsole() {
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -10, scale: 0.97 }}
transition={{ duration: 0.4, ease: EASE_OUT_EXPO }}
className={`flex flex-col ${isBot ? "items-end" : "items-start"} w-full`}
className={`flex flex-col ${isBot ? "items-start" : "items-end"} w-full`}
>
<div className={`flex items-center gap-1.5 mb-1 text-[11px] font-bold text-ink-muted ${isBot ? "flex-row-reverse" : ""}`}>
<div className={`flex items-center gap-1.5 mb-1 text-[11px] font-bold text-ink-muted ${isBot ? "" : "flex-row-reverse"}`}>
<span className={`flex items-center gap-1 ${isBot ? "text-primary" : "text-ink-soft"}`}>
{isBot ? (
<>
@ -161,7 +197,7 @@ export function NegotiationConsole() {
<div
className={`max-w-[85%] rounded-[20px] p-4 text-sm leading-relaxed font-semibold shadow-[0_2px_8px_rgba(0,0,0,0.02)] transition-all duration-300 ${
isBot ? "bg-primary-soft text-ink rounded-tr-none" : "bg-white text-ink rounded-tl-none"
isBot ? "bg-primary-soft text-ink rounded-tl-none" : "bg-white text-ink rounded-tr-none"
}`}
>
<SlateRenderer nodes={item.editorNodes} />
@ -180,11 +216,63 @@ export function NegotiationConsole() {
)
}
// 데모 대화 시나리오 — 1,200원 제시에서 1,050원 낙찰까지 6스텝
// 시작가(첫 제시가)·목표가 — 좌측 패널 델타·스파크라인 기준값
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[] = [
{
step: 1,
price: 1200,
price: 1200000,
editorNodes: [
{
type: 'paragraph',
@ -192,14 +280,14 @@ const NEGOTIATION_STEPS: DialogueStep[] = [
children: [
{ text: '협력사', bold: true },
{ text: ' : ' },
{ text: '현재 글로벌 원재료 상승 요인으로 제안할 수 있는 최선의 단가는 1,200원입니다. 이 이하로는 마진 확보가 어렵습니다.' },
{ text: '현재 글로벌 원재료 상승 요인으로 제안할 수 있는 최선의 단가는 1,200,000원입니다. 이 이하로는 마진 확보가 어렵습니다.' },
],
},
],
},
{
step: 2,
price: 1150,
price: 1150000,
editorNodes: [
{
type: 'paragraph',
@ -207,7 +295,7 @@ const NEGOTIATION_STEPS: DialogueStep[] = [
children: [
{ text: 'AI 흥정 봇', bold: true },
{ text: ' : ' },
{ text: '제시해주신 1,200원은 당사 타 유사 품목 이력 및 시중 원가 인덱스 데이터 대비 약 8.3% 높게 책정되어 있습니다. ', italic: true },
{ text: '제시해주신 1,200,000원은 당사 타 유사 품목 이력 및 시중 원가 인덱스 데이터 대비 약 8.3% 높게 책정되어 있습니다. ', italic: true },
{ text: '상호 호혜적 장기 계약 체결을 전제로 조율가 범위를 반영해 제안해 드립니다.', code: true },
],
},
@ -215,7 +303,7 @@ const NEGOTIATION_STEPS: DialogueStep[] = [
},
{
step: 3,
price: 1150,
price: 1150000,
editorNodes: [
{
type: 'paragraph',
@ -223,14 +311,14 @@ const NEGOTIATION_STEPS: DialogueStep[] = [
children: [
{ text: '협력사', bold: true },
{ text: ' : ' },
{ text: '제조 공정상 급격한 인하는 무리가 있으나, 상생 협력 차원에서 1,150원까지는 즉시 조정해 드릴 용의가 있습니다.' },
{ text: '제조 공정상 급격한 인하는 무리가 있으나, 상생 협력 차원에서 1,150,000원까지는 즉시 조정해 드릴 용의가 있습니다.' },
],
},
],
},
{
step: 4,
price: 1080,
price: 1080000,
editorNodes: [
{
type: 'paragraph',
@ -238,14 +326,14 @@ const NEGOTIATION_STEPS: DialogueStep[] = [
children: [
{ text: 'AI 흥정 봇', bold: true },
{ text: ' : ' },
{ text: '적극적인 협조에 감사드립니다. 만약 연간 최소 발주 수량을 보증하고 공급망 일정을 다소 유연화해주신다면, 목표가인 1,080원 선까지 맞출 수 있을까요?', italic: true },
{ text: '적극적인 협조에 감사드립니다. 만약 연간 최소 발주 수량을 보증하고 공급망 일정을 다소 유연화해주신다면, 목표가인 1,080,000원 선까지 맞출 수 있을까요?', italic: true },
],
},
],
},
{
step: 5,
price: 1050,
price: 1050000,
editorNodes: [
{
type: 'paragraph',
@ -254,14 +342,14 @@ const NEGOTIATION_STEPS: DialogueStep[] = [
{ text: '협력사', bold: true },
{ text: ' : ' },
{ text: '좋습니다. 제안하신 연간 개런티 확보 및 대금 현금 결제 기한 단축을 승인해 주시는 조건으로, ' },
{ text: '최종 조율가 1,050원으로 맞춰서 계약을 체결하겠습니다.', bold: true },
{ text: '최종 조율가 1,050,000원으로 맞춰서 계약을 체결하겠습니다.', bold: true },
],
},
],
},
{
step: 6,
price: 1050,
price: 1050000,
badge: '낙찰 성공 · 12.5% 예산 절감',
editorNodes: [
{
@ -270,7 +358,7 @@ const NEGOTIATION_STEPS: DialogueStep[] = [
children: [
{ text: 'AI 흥정 봇', bold: true },
{ text: ' : ' },
{ text: '최종 합의 접수 완료 — 가이드 상한가(1,200원) 대비 합의 낙찰가 1,050원으로 최종 계약 승인 처리 완료되었습니다. ', bold: true },
{ text: '최종 합의 접수 완료 — 가이드 상한가(1,200,000원) 대비 합의 낙찰가 1,050,000원으로 최종 계약 승인 처리 완료되었습니다. ', bold: true },
{ text: '본 흥정 마일스톤 및 단가 타결 히스토리는 사내 투명성 보증을 위해 보존 기록됩니다.', code: true },
],
},

View File

@ -18,10 +18,10 @@ export function Reinforcement() {
title="협상할수록, 더 좋은 조건으로"
description={
<>
negotium은 {" "}
<span className="font-bold text-primary"> </span> .
,
.
릿 , . negotium은
<span className="font-bold text-primary"> </span> .
,
.
</>
}
descriptionClassName="mt-8 max-w-3xl"
@ -62,17 +62,17 @@ function PillarCard({ pillar, delay }: { pillar: Pillar; delay: number }) {
const PILLARS: Pillar[] = [
{
icon: Scale,
title: "흥정의 일관성 보존",
description: "개인 감정이나 친분에 휘둘리지 않고 사내 재무 원칙과 가이드라인을 완벽히 관철시킵니다.",
title: "협상 기준의 일관성",
description: "개인 감정이나 친분에 휘둘리지 않고, 사내 기준과 가이드라인대로만 협상합니다.",
},
{
icon: RefreshCw,
title: "지속적인 전략 최적화",
description: "매 조율 피드백을 축적하여 상대방의 마진 수용성 확률 지도를 고도화해 나갑니다.",
description: "전략 강화 학습을 통해 기준선이 상승하고, 점진적으로 개선됩니다. 시간이 곧 협상력이 됩니다.",
},
{
icon: GitMerge,
title: "낙찰 성사율 극대화",
description: "단순한 협상 결렬(No-deal) 리스크를 상시 추산해 안전하면서도 최고 이익률의 균형점을 확보합니다.",
description: "무리하게 후려쳐 관계를 깨는 대신, 성사되는 선에서 최대한 끌어냅니다.",
},
]

View File

@ -1,7 +1,9 @@
import { useState } from "react";
import { Comparison } from "@/components/sections/comparison";
import { Contact } from "@/components/sections/contact";
import { CoreValues } from "@/components/sections/core-values";
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";
@ -10,7 +12,6 @@ import { HeroNeumorphic } from "@/components/sections/hero-neumorphic";
import { HowItWorksDemo } from "@/components/sections/how-it-works-demo";
import { NegotiationConsole } from "@/components/sections/negotiation-console";
import { Reinforcement } from "@/components/sections/reinforcement";
import { ROISimulator } from "@/components/sections/roi-simulator";
const TITLE = "negotium — AI 구매 협상 자동화";
const DESCRIPTION =
@ -38,9 +39,8 @@ export default function Home() {
<div className="min-h-screen bg-white text-ink font-sans antialiased selection:bg-primary/10 selection:text-primary">
<Header />
{import.meta.env.DEV && <HeroThemeSwitcher value={heroTheme} onChange={setHeroTheme} />}
<div className="relative">
{import.meta.env.DEV && <HeroThemeSwitcher value={heroTheme} onChange={setHeroTheme} />}
{heroTheme === "neumorphic" && <HeroNeumorphic />}
{heroTheme === "glassmorphic" && <HeroGlassmorphic />}
</div>
@ -49,7 +49,8 @@ export default function Home() {
<HowItWorksDemo />
<Reinforcement />
<CoreValues />
<ROISimulator />
<Comparison />
<Faq />
<FinalCTA />
<Contact />
<Footer />
@ -58,13 +59,13 @@ export default function Home() {
}
const HERO_OPTIONS: { value: HeroTheme; label: string }[] = [
{ value: "neumorphic", label: "Neumorphic" },
{ value: "neumorphic", label: "Phone Demo" },
{ value: "glassmorphic", label: "Soft Ambient" },
];
function HeroThemeSwitcher({ value, onChange }: { value: HeroTheme; onChange: (theme: HeroTheme) => void }) {
return (
<div className="fixed top-24 left-1/2 -translate-x-1/2 z-50 flex items-center gap-1.5 p-1 rounded-full shadow-[0_4px_20px_rgba(0,0,0,0.06)] border border-line-strong bg-white/95 text-ink-soft backdrop-blur-md text-xs font-semibold">
<div className="absolute top-24 left-1/2 -translate-x-1/2 z-30 flex items-center gap-1.5 p-1 rounded-full shadow-[0_4px_20px_rgba(0,0,0,0.06)] border border-line-strong bg-white/95 text-ink-soft backdrop-blur-md text-xs font-semibold">
<span className="pl-3.5 pr-1.5 text-[10px] uppercase tracking-wider text-ink-muted font-bold">Hero Design:</span>
{HERO_OPTIONS.map((option) => (
<button