diff --git a/landing/app/components/sections/negotiation-demo.tsx b/landing/app/components/sections/negotiation-demo.tsx index 84c0ee3..7ad9b02 100644 --- a/landing/app/components/sections/negotiation-demo.tsx +++ b/landing/app/components/sections/negotiation-demo.tsx @@ -41,7 +41,7 @@ export function NegotiationDemo() { const [offer, setOffer] = useState(DEMO_ITEMS[0].listPrice) /* 입력란의 원문. offer 와 따로 두는 이유는 타이핑 도중 상태다 — 숫자 하나로 묶으면 "1" 을 치는 순간 범위 밖이라 튕겨서 사용자와 싸우게 된다. 확정은 blur/Enter 에서 한다. */ - const [offerText, setOfferText] = useState(String(DEMO_ITEMS[0].listPrice)) + const [offerText, setOfferText] = useState(won(DEMO_ITEMS[0].listPrice)) const [finalPrice, setFinalPrice] = useState(DEMO_ITEMS[0].listPrice) const logRef = useRef(null) @@ -69,7 +69,7 @@ export function NegotiationDemo() { setFinalPrice(nextItem.listPrice) const start = nextRole === "seller" ? nextItem.listPrice : nextItem.marketLow setOffer(start) - setOfferText(String(start)) + setOfferText(won(start)) } const pickRole = (r: Role) => { @@ -104,7 +104,7 @@ export function NegotiationDemo() { setCardsUsed((c) => c + 1) // 에이전트가 부른 값에서 다시 시작. 입력란도 같이 갱신해야 슬라이더와 숫자가 어긋나지 않는다. setOffer(res.price) - setOfferText(String(res.price)) + setOfferText(won(res.price)) } }, 620) timers.current.push(id) @@ -147,7 +147,9 @@ export function NegotiationDemo() { if (inputLocked) return const next = Number.isFinite(raw) && raw > 0 ? round10(Math.min(sliderMax, Math.max(sliderMin, raw))) : offer setOffer(next) - setOfferText(String(next)) + /* 확정된 값은 콤마를 넣어 보여준다 — 화면의 다른 금액이 전부 won() 이라 + 여기만 1018800 으로 나오면 같은 값으로 안 읽힌다. 타이핑 중에는 숫자만 남긴다. */ + setOfferText(won(next)) } return ( @@ -280,11 +282,11 @@ export function NegotiationDemo() { aria-label={role === "seller" ? "제시 단가 직접 입력" : "목표 단가 직접 입력"} value={offerText} onChange={(e) => setOfferText(e.target.value.replace(/[^\d]/g, ""))} - onBlur={() => commitOffer(Number(offerText))} + onBlur={() => commitOffer(Number(offerText.replace(/[^\d]/g, "")))} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault() - commitOffer(Number(offerText)) + commitOffer(Number(offerText.replace(/[^\d]/g, ""))) } }} disabled={inputLocked} diff --git a/landing/app/lib/negotiation-sim.ts b/landing/app/lib/negotiation-sim.ts index ab9f32d..51bff6b 100644 --- a/landing/app/lib/negotiation-sim.ts +++ b/landing/app/lib/negotiation-sim.ts @@ -114,10 +114,22 @@ export function respondToOffer( const ask = round10(item.anchor + (offer - item.anchor) * CONCESSION[cardsUsed]) const lever = LEVERS[cardsUsed] + /* 근거는 반드시 상대가 제시한 값을 물고 있어야 한다. + 예전엔 첫 카드에서 marketLow 만 읊어서, 얼마를 제시하든 같은 문장이 나왔다. + CONCESSION[0] = 0 이라 부르는 값(ask)까지 앵커로 고정이라, 슬라이더를 움직여도 + 화면이 전혀 반응하지 않는 것처럼 보였다. 값은 고수하되(그게 이 에이전트의 성격이다) + 무엇에 대한 응수인지는 매번 달라져야 한다. */ + const gap = offer - ask const reason = - cardsUsed === 0 - ? `같은 사양 인터넷 최저가가 ${won(item.marketLow)}원입니다.` - : `${won(offer)}원과 저희 기준 사이가 아직 ${won(offer - ask)}원 남았습니다.` + cardsUsed > 0 + ? `${won(offer)}원과 저희 기준 사이가 아직 ${won(gap)}원 남았습니다.` + : offer >= item.listPrice + ? `정가 그대로는 검토가 어렵습니다. 같은 사양 인터넷 최저가가 ${won(item.marketLow)}원입니다.` + : offer > item.marketLow + ? `${won(offer)}원은 같은 사양 인터넷 최저가 ${won(item.marketLow)}원보다 ${won( + offer - item.marketLow, + )}원 높습니다.` + : `${won(offer)}원이면 시장 최저가 선까지 내려오셨습니다. 다만 이번 견적 기준까지 ${won(gap)}원 남았습니다.` return { outcome: "running",