diff --git a/landing/app/components/sections/hero-dataflow.tsx b/landing/app/components/sections/hero-dataflow.tsx
index eee2c8d..f04c45e 100644
--- a/landing/app/components/sections/hero-dataflow.tsx
+++ b/landing/app/components/sections/hero-dataflow.tsx
@@ -66,12 +66,14 @@ export function HeroDataFlow() {
{/* 히어로 카피에는 진입 애니메이션을 걸지 않는다.
여기가 LCP 요소라 opacity:0 으로 시작하면 하이드레이션 전까지 첫 화면이 비고,
광고 유입처럼 JS 가 느린 환경에서 그대로 손실이 된다. 모션은 배경 영상이 맡는다. */}
- {/* 2톤 헤드라인의 약한 쪽은 별도 회색이 아니라 같은 흰색의 투명도로 낮춘다.
- 네이비 위에 라벤더 회색을 얹으면 화면에 색상각이 하나 더 생겨 탁해진다. */}
+ {/* 여기는 2톤을 쓰지 않는다. 예전 캔버스 배경은 하단 밴드에만 입자가 있어 헤드라인
+ 뒤가 거의 단색이었고, 그때는 투명도 60% 로 낮춘 둘째 줄도 읽혔다. 배경이 영상으로
+ 바뀌면서 밝은 요소가 글자 뒤로 지나가 그 대비가 무너졌다 — 배경이 움직이는 위에서는
+ 가독성이 조판 장치보다 우선한다. 2톤은 최종 CTA 에 남아 있다(그쪽은 배경이 정적). */}
협력사 전원과 1:1로,
- 동시에 협상합니다.
+ 동시에 협상합니다.
{/* 히어로는 h1 이어야 해서 SectionHeading(h2 고정)을 못 쓴다. 대신 간격만은
diff --git a/landing/app/components/sections/negotiation-demo.tsx b/landing/app/components/sections/negotiation-demo.tsx
index a68416d..84c0ee3 100644
--- a/landing/app/components/sections/negotiation-demo.tsx
+++ b/landing/app/components/sections/negotiation-demo.tsx
@@ -39,6 +39,9 @@ export function NegotiationDemo() {
const [cardsUsed, setCardsUsed] = useState(0)
const [outcome, setOutcome] = useState("running")
const [offer, setOffer] = useState(DEMO_ITEMS[0].listPrice)
+ /* 입력란의 원문. offer 와 따로 두는 이유는 타이핑 도중 상태다 — 숫자 하나로 묶으면
+ "1" 을 치는 순간 범위 밖이라 튕겨서 사용자와 싸우게 된다. 확정은 blur/Enter 에서 한다. */
+ const [offerText, setOfferText] = useState(String(DEMO_ITEMS[0].listPrice))
const [finalPrice, setFinalPrice] = useState(DEMO_ITEMS[0].listPrice)
const logRef = useRef(null)
@@ -64,7 +67,9 @@ export function NegotiationDemo() {
setCardsUsed(0)
setOutcome("running")
setFinalPrice(nextItem.listPrice)
- setOffer(nextRole === "seller" ? nextItem.listPrice : nextItem.marketLow)
+ const start = nextRole === "seller" ? nextItem.listPrice : nextItem.marketLow
+ setOffer(start)
+ setOfferText(String(start))
}
const pickRole = (r: Role) => {
@@ -97,7 +102,9 @@ export function NegotiationDemo() {
setOutcome(res.outcome)
if (res.outcome === "running") {
setCardsUsed((c) => c + 1)
- setOffer(res.price) // 에이전트가 부른 값에서 다시 시작
+ // 에이전트가 부른 값에서 다시 시작. 입력란도 같이 갱신해야 슬라이더와 숫자가 어긋나지 않는다.
+ setOffer(res.price)
+ setOfferText(String(res.price))
}
}, 620)
timers.current.push(id)
@@ -123,6 +130,26 @@ export function NegotiationDemo() {
const sliderMin = role === "seller" ? round10(item.anchor * 0.94) : round10(item.anchor * 0.94)
const sliderMax = role === "seller" ? item.listPrice : item.marketLow
+ /* 협상이 시작된 뒤 구매자는 목표가를 못 바꾼다 — 목표가를 정하고 위임하는 게 제품의 전제다.
+ 공급사는 카드가 남아 있는 한 계속 제시할 수 있다. */
+ const inputLocked = role === "buyer" && turns.length > 0
+
+ /* 추천가는 시뮬레이터가 실제로 쓰는 수치에서 가져온다. 지어낸 숫자를 추천이라 부르면
+ 사용자가 그대로 넣었을 때 결과가 설명되지 않는다. */
+ const recommended = role === "seller" ? item.marketLow : item.anchor
+ const recommendReason =
+ role === "seller"
+ ? `같은 사양 시장 최저가 선입니다. 여기서부터 에이전트가 근거를 들고 응수합니다.`
+ : `결렬 없이 도달 가능한 최저선입니다. 더 낮추면 협상이 성사되지 않습니다.`
+
+ /** 슬라이더·직접입력·추천적용이 모두 거쳐가는 단일 확정 경로. 범위로 당기고 10원 단위로 맞춘다. */
+ const commitOffer = (raw: number) => {
+ if (inputLocked) return
+ const next = Number.isFinite(raw) && raw > 0 ? round10(Math.min(sliderMax, Math.max(sliderMin, raw))) : offer
+ setOffer(next)
+ setOfferText(String(next))
+ }
+
return (
{/* 머리는 손으로 짜지 않고 SectionHeading 을 쓴다. 예전엔 여기서 직접 조판하느라
@@ -219,21 +246,63 @@ export function NegotiationDemo() {
{!done && (
+ {/* 에이전트 추천가. 값을 지어내지 않고 시뮬레이터가 실제로 쓰는 수치를 쓴다 —
+ 구매자는 anchor(결렬 없이 닿는 최저선, reachable = target >= anchor),
+ 공급사는 marketLow(에이전트가 응수할 때 인용하는 값). 추천만 하고
+ 확정은 사용자가 한다. 그게 이 데모가 보여줘야 할 관계다. */}
+
+
+
에이전트 추천
+
{recommendReason}
+
+
+
+
- {won(round10(offer))}원
+ {/* 직접 입력. 슬라이더만 두면 정확한 값을 못 넣는다 — 실제 구매 담당자는
+ "얼마까지"라는 숫자를 이미 들고 온다. 타이핑 중에는 clamp 하지 않고
+ blur/Enter 에서 확정한다. 입력 즉시 범위로 당기면 손가락과 싸운다. */}
+