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" import type { DialogueStep } from "@/types" /** 스크롤 연동 sticky 협상 데모 — 스크롤 진행률에 따라 대화가 쌓이고 단가가 내려간다. */ export function NegotiationConsole() { // -1 = 아직 섹션 진입 전(온보딩 가이드 표시) const [activeStep, setActiveStep] = useState(-1) const [animatedPrice, setAnimatedPrice] = useState(1200000) const sectionRef = useRef(null) const chatContainerRef = useRef(null) // 섹션 내 스크롤 진행률 → 대화 스텝 매핑 useEffect(() => { const handleScroll = () => { if (!sectionRef.current) return const rect = sectionRef.current.getBoundingClientRect() const totalScrollable = rect.height - window.innerHeight if (totalScrollable <= 0) return const scrolled = -rect.top if (scrolled < 0) { setActiveStep(-1) return } const clampedRatio = Math.max(0, Math.min(scrolled / totalScrollable, 0.99)) setActiveStep(Math.floor(clampedRatio * NEGOTIATION_STEPS.length)) } window.addEventListener("scroll", handleScroll, { passive: true }) handleScroll() return () => window.removeEventListener("scroll", handleScroll) }, []) // 단가 오도미터 애니메이션 (500ms ease-out) useEffect(() => { const targetPrice = activeStep === -1 ? 1200000 : NEGOTIATION_STEPS[activeStep].price if (animatedPrice === targetPrice) return const start = animatedPrice const duration = 500 const startTime = performance.now() const animate = (currentTime: number) => { const progress = Math.min((currentTime - startTime) / duration, 1) const easeProgress = progress * (2 - progress) setAnimatedPrice(Math.round(start + (targetPrice - start) * easeProgress)) if (progress < 1) requestAnimationFrame(animate) } requestAnimationFrame(animate) }, [activeStep]) // 새 말풍선이 붙으면 채팅 영역을 바닥으로 스크롤 useEffect(() => { chatContainerRef.current?.scrollTo({ top: chatContainerRef.current.scrollHeight, behavior: "smooth" }) }, [activeStep]) return (
{/* 섹션 헤딩 — sticky 상단 고정 (모바일은 공간상 생략) */}
Live Replay 봇은 물러서지 않습니다
{/* 좌: 실시간 단가 패널 */}
실시간 협상 단가
₩ {animatedPrice.toLocaleString()}
{/* 시작가 대비 누적 절감 델타 — 높이 고정으로 레이아웃 점프 방지 */}
{animatedPrice < START_PRICE && ( ▼ {(START_PRICE - animatedPrice).toLocaleString()}원 (−{(((START_PRICE - animatedPrice) / START_PRICE) * 100).toFixed(1)}%) )}
{/* 계단식 하락 스파크라인 + 목표가 기준선 */}
step.price)} targetPrice={TARGET_PRICE} progress={activeStep < 0 ? 0 : (activeStep + 1) / NEGOTIATION_STEPS.length} />
시작가 {START_PRICE.toLocaleString()} 목표가 {TARGET_PRICE.toLocaleString()}
실제 협상 화면을 재구성한 데모입니다.
{activeStep !== -1 && NEGOTIATION_STEPS[activeStep]?.badge && ( {NEGOTIATION_STEPS[activeStep].badge} )}
{/* 우: 대화 말풍선 (봇 좌 / 파트너 우) */}
{activeStep === -1 ? (

실시간 AI 협상 대화

화면을 스크롤하면 실시간 단가 조율 대화가 시작됩니다.

마우스 휠 스크롤하기 ↓
) : (
{NEGOTIATION_STEPS.slice(0, activeStep + 1).map((item, idx) => { const isBot = item.editorNodes[0].sender === "bot" return (
{isBot ? ( <> NEGOTIUM BOT ) : ( <> PARTNER )}
) })}
)}
) } // 시작가(첫 제시가)·목표가 — 좌측 패널 델타·스파크라인 기준값 const START_PRICE = 1200000 const TARGET_PRICE = 1080000 // 데모 대화 시나리오 — 1,200,000원 제시에서 1,050,000원 낙찰까지 6스텝 const NEGOTIATION_STEPS: DialogueStep[] = [ { step: 1, price: 1200000, editorNodes: [ { type: 'paragraph', sender: 'supplier', children: [ { text: '협력사', bold: true }, { text: ' : ' }, { text: '현재 글로벌 원재료 상승 요인으로 제안할 수 있는 최선의 단가는 1,200,000원입니다. 이 이하로는 마진 확보가 어렵습니다.' }, ], }, ], }, { step: 2, price: 1150000, editorNodes: [ { type: 'paragraph', sender: 'bot', children: [ { text: 'AI 흥정 봇', bold: true }, { text: ' : ' }, { text: '제시해주신 1,200,000원은 당사 타 유사 품목 이력 및 시중 원가 인덱스 데이터 대비 약 8.3% 높게 책정되어 있습니다. ', italic: true }, { text: '상호 호혜적 장기 계약 체결을 전제로 조율가 범위를 반영해 제안해 드립니다.', code: true }, ], }, ], }, { step: 3, price: 1150000, editorNodes: [ { type: 'paragraph', sender: 'supplier', children: [ { text: '협력사', bold: true }, { text: ' : ' }, { text: '제조 공정상 급격한 인하는 무리가 있으나, 상생 협력 차원에서 1,150,000원까지는 즉시 조정해 드릴 용의가 있습니다.' }, ], }, ], }, { step: 4, price: 1080000, editorNodes: [ { type: 'paragraph', sender: 'bot', children: [ { text: 'AI 흥정 봇', bold: true }, { text: ' : ' }, { text: '적극적인 협조에 감사드립니다. 만약 연간 최소 발주 수량을 보증하고 공급망 일정을 다소 유연화해주신다면, 목표가인 1,080,000원 선까지 맞출 수 있을까요?', italic: true }, ], }, ], }, { step: 5, price: 1050000, editorNodes: [ { type: 'paragraph', sender: 'supplier', children: [ { text: '협력사', bold: true }, { text: ' : ' }, { text: '좋습니다. 제안하신 연간 개런티 확보 및 대금 현금 결제 기한 단축을 승인해 주시는 조건으로, ' }, { text: '최종 조율가 1,050,000원으로 맞춰서 계약을 체결하겠습니다.', bold: true }, ], }, ], }, { step: 6, price: 1050000, badge: '낙찰 성공 · 12.5% 예산 절감', editorNodes: [ { type: 'paragraph', sender: 'bot', children: [ { text: 'AI 흥정 봇', bold: true }, { text: ' : ' }, { text: '최종 합의 접수 완료 — 가이드 상한가(1,200,000원) 대비 합의 낙찰가 1,050,000원으로 최종 계약 승인 처리 완료되었습니다. ', bold: true }, { text: '본 흥정 마일스톤 및 단가 타결 히스토리는 사내 투명성 보증을 위해 보존 기록됩니다.', code: true }, ], }, ], }, ]