- KT-NEGOWIZ /chat 을 현재 아키텍처(Vite·React Router·feature 구조)로 이식 - 사이드 ItemSection(+이미지 확대), 헤더 잔여시간, 채팅(메시지/템플릿/입력), 메뉴 섹션 - 메시지 템플릿: 협상 성공률·협상요약·투찰요약·재협상/재견적 폼 - zustand 스토어 + mock 데이터(정적 쇼케이스, 제출 시 로컬 append) - Next.js·KT 의존 제거(next/image·navigation, 로고·서비스명·연락처, 팝업 보류) - ChatPage 배선: ChatContainer / ItemSection / RemainingTime + auth SidebarFooter 재사용 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
116 lines
5.3 KiB
TypeScript
116 lines
5.3 KiB
TypeScript
import { useState, useMemo } from 'react'
|
|
import { cn } from '@/lib'
|
|
import { numberToKorean } from '@/features/chat/lib/koreanNumber'
|
|
import { findRestoreScript, extractPart } from '@/features/chat/lib/rejectForm'
|
|
import { SelectRadio, SubmitButton } from '@/features/chat/components/templates/rejectControls'
|
|
import { useChatStore } from '@/features/chat/stores/useChatStore'
|
|
import { useChatInitStore } from '@/features/chat/stores/useChatInitStore'
|
|
|
|
const MAX_PRICE = 999999999999999
|
|
|
|
// 재견적 실패 시: 최종 공급 희망 가격 + 배송 형태 입력 폼
|
|
export function RejectCM() {
|
|
const isLoading = useChatStore((s) => s.isLoading)
|
|
const sendMessage = useChatStore((s) => s.sendMessage)
|
|
const messages = useChatStore((s) => s.messages)
|
|
const item_vat_yn = useChatInitStore((s) => s.item_vat_yn)
|
|
const isVAT = item_vat_yn === 'VAT별도'
|
|
|
|
// 폼 값은 이전 제출 내역에서 1회만 복원 (setter 미사용 = lazy 초기화 전용)
|
|
const restored = useState(() => findRestoreScript(messages, 'rejectCM'))[0]
|
|
const [price, setPrice] = useState(() => extractPart(restored, '공급희망가격-'))
|
|
const [selectedReason, setSelectedReason] = useState(() => extractPart(restored, '배송형태-'))
|
|
const [priceErrorMessage, setPriceErrorMessage] = useState('')
|
|
const [radioErrorMessage, setRadioErrorMessage] = useState('')
|
|
|
|
// 제출 완료 여부는 messages 변화에 반응해야 함 (제출 후 갱신 반영)
|
|
const isSubmitted = useMemo(() => Boolean(findRestoreScript(messages, 'rejectCM')), [messages])
|
|
const isValid = Boolean(price && parseInt(price) > 0) && Boolean(selectedReason)
|
|
|
|
const handlePriceChange = (value: string) => {
|
|
const numeric = value.replace(/\D/g, '')
|
|
if (numeric && parseInt(numeric) > MAX_PRICE) return
|
|
setPrice(numeric)
|
|
if (numeric) setPriceErrorMessage('')
|
|
}
|
|
|
|
const koreanPrice = (() => {
|
|
if (!price) return '영'
|
|
const n = parseInt(price)
|
|
return Number.isNaN(n) || n === 0 ? '영' : numberToKorean(n)
|
|
})()
|
|
|
|
const handleSubmit = () => {
|
|
setPriceErrorMessage(!price || parseInt(price) === 0 ? '가격을 입력해주세요.' : '')
|
|
setRadioErrorMessage(!selectedReason ? '배송 형태를 선택해주세요.' : '')
|
|
if (isValid && !isLoading && !isSubmitted) {
|
|
sendMessage(`공급희망가격-${price}, 배송형태-${selectedReason}`, 'text')
|
|
}
|
|
}
|
|
|
|
const isDisabled = isLoading || isSubmitted
|
|
|
|
return (
|
|
<div className="flex flex-col w-full p-10 bg-neutral-00 rounded-[28px] gap-2 transition-all duration-300">
|
|
<div className="flex w-full h-[48px] bg-[#CD2A2D] headline-3 text-neutral-00 rounded-[12px] items-center justify-center">
|
|
최종 공급 희망 가격 입력
|
|
</div>
|
|
|
|
<div className="reject break-keep py-2">
|
|
최종 공급 희망 가격과 배송 형태를 입력하여 주시기 바랍니다.
|
|
<br />
|
|
가격 검토를 통해 경쟁력 있는 가격일 경우 다시 협상에 참여할 기회가 주어지오니 신중한 가격 제안 부탁드립니다.
|
|
</div>
|
|
|
|
<div className="flex flex-col w-full gap-1">
|
|
<div className="flex w-full items-start gap-4">
|
|
<div className="flex reject-2 whitespace-nowrap pt-2 flex-shrink-0 w-[110px]">공급 희망 가격</div>
|
|
<div className="flex flex-wrap w-full items-center pt-2">
|
|
<div className="flex items-center gap-2">
|
|
<input
|
|
type="text"
|
|
className={cn(
|
|
'text-right max-w-[220px] h-full max-h-[35px] border p-2 body-3 placeholder:text-neutral-60 focus:border-neutral-70 rounded-[8px] outline-none border-neutral-40',
|
|
isDisabled ? 'bg-neutral-10 text-neutral-60 cursor-not-allowed' : 'bg-white text-neutral-90',
|
|
)}
|
|
value={price ? parseInt(price).toLocaleString() : ''}
|
|
onChange={(e) => handlePriceChange(e.target.value)}
|
|
placeholder="0"
|
|
disabled={isDisabled}
|
|
/>
|
|
<div className="reject whitespace-nowrap mr-2">원{isVAT && '(VAT 별도)'}</div>
|
|
</div>
|
|
<div className="reject-gray whitespace-nowrap">[{koreanPrice} 원]</div>
|
|
</div>
|
|
</div>
|
|
{priceErrorMessage && <div className="body-5 text-negative pl-[126px]">{priceErrorMessage}</div>}
|
|
</div>
|
|
|
|
<div className="flex w-full gap-4">
|
|
<div className="flex reject-2 whitespace-nowrap pt-2 flex-shrink-0 w-[110px]">배송 형태</div>
|
|
<div className="flex w-full items-center justify-between pr-20 flex-wrap">
|
|
{['협력사배송', '지정택배배송', '픽업배송'].map((opt) => (
|
|
<SelectRadio
|
|
key={opt}
|
|
text={opt === '협력사배송' ? '협력사 배송' : opt}
|
|
value={opt}
|
|
name="rejectcm"
|
|
selectedValue={selectedReason}
|
|
onChange={(v) => {
|
|
setSelectedReason(v)
|
|
setRadioErrorMessage('')
|
|
}}
|
|
errorMessage={radioErrorMessage}
|
|
disabled={isDisabled}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex w-full items-center justify-center mt-4">
|
|
<SubmitButton isValid={isValid} isLoading={isLoading} isSubmitted={isSubmitted} onSubmit={handleSubmit} />
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|