- 반응형: 최소 폭 1350→1024px, max-[1350px]/max-[1180px] 2단계 축소 - 사이드바 350→310→280, 우측 메뉴 360→320→280, 채팅 거터 140/126→80/72→48/40 (헤더 패딩 동기) - 가격·할인율 입력 min-w 480→420→320, 가운데 버튼 패딩·최소폭 축소, 상품목록 버튼 1180px 미만 아이콘만 - 상품 이미지 220→200, 사이드바 정보 행 값 영역 고정 150px → flex-1 - 리드타임: formatLeadTime 헬퍼 신설 — 좌측 상품정보·협상 결과 요약 카드에 "일" 접미 표시 - 인디케이터: init 의 session_status 를 스토어에 적재, 협상중(IN_PROGRESS) 세션에서만 표시 - 사이드바 정보 영역 의미 없는 가로 스크롤 제거 (shrink 금지 컬럼 + overflow-x 자동 계산이 원인) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
158 lines
4.7 KiB
TypeScript
158 lines
4.7 KiB
TypeScript
import { useState, useRef, useId } from 'react'
|
|
import { useChatStore } from '@/features/chat/stores/useChatStore'
|
|
|
|
// 단위(원/%) 오버레이 + 전송 버튼이 붙은 값 입력 (Percent / Price 공용)
|
|
function InputWithUnit({
|
|
value,
|
|
onChange,
|
|
onSubmit,
|
|
placeholder,
|
|
unit,
|
|
error,
|
|
inputRef,
|
|
ariaLabel,
|
|
}: {
|
|
value: string
|
|
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void
|
|
onSubmit: () => void
|
|
placeholder: string
|
|
unit: string
|
|
error: string
|
|
inputRef: React.RefObject<HTMLInputElement | null>
|
|
ariaLabel: string
|
|
}) {
|
|
const errorId = useId()
|
|
return (
|
|
<div className="relative flex flex-col gap-3">
|
|
<div className="relative flex items-center min-w-[480px] max-[1350px]:min-w-[420px] max-[1180px]:min-w-[320px]">
|
|
<input
|
|
ref={inputRef}
|
|
type="text"
|
|
inputMode="decimal"
|
|
value={value}
|
|
onChange={onChange}
|
|
onKeyDown={(e) => e.key === 'Enter' && onSubmit()}
|
|
className="h-[50px] flex-1 bg-white border-none rounded-[999px] outline-none ring-1 ring-primary headline-3 text-negative placeholder:text-neutral-60 pl-[32px]"
|
|
placeholder={placeholder}
|
|
aria-label={ariaLabel}
|
|
aria-invalid={!!error}
|
|
aria-describedby={error ? errorId : undefined}
|
|
/>
|
|
<div className="absolute left-[32px] headline-3 text-neutral-70 pointer-events-none">
|
|
<span className="opacity-0">{value}</span>
|
|
{value && <span className="ml-1">{unit}</span>}
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={onSubmit}
|
|
className="absolute right-0 flex items-center justify-center min-w-[120px] h-[50px] px-[32px] bg-primary hover:brightness-[0.97] rounded-[999px] title-2 text-primary-foreground cursor-pointer whitespace-nowrap transition-colors duration-200"
|
|
aria-label="전송"
|
|
>
|
|
전송
|
|
</button>
|
|
</div>
|
|
{error && (
|
|
<div id={errorId} role="alert" className="absolute top-[62px] left-1/2 -translate-x-1/2 title-2 text-negative w-full text-center">
|
|
{error}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function Percent() {
|
|
const [percent, setPercent] = useState('')
|
|
const [error, setError] = useState('')
|
|
const inputRef = useRef<HTMLInputElement>(null)
|
|
const sendMessage = useChatStore((s) => s.sendMessage)
|
|
|
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const value = e.target.value
|
|
if (value === '') {
|
|
setPercent('')
|
|
setError('')
|
|
return
|
|
}
|
|
if (!/^\d*\.?\d*$/.test(value)) return
|
|
if (value.includes('.')) {
|
|
const parts = value.split('.')
|
|
if (parts[1] && parts[1].length > 1) return
|
|
} else if (value.length > 2) {
|
|
return
|
|
}
|
|
if (parseFloat(value) > 80) return
|
|
setPercent(value)
|
|
setError('')
|
|
}
|
|
|
|
const handleSubmit = () => {
|
|
if (!percent || percent === '.') {
|
|
setError('숫자를 입력해주세요')
|
|
return
|
|
}
|
|
sendMessage(`${parseFloat(percent).toString()}%`, 'percent')
|
|
setError('')
|
|
}
|
|
|
|
return (
|
|
<InputWithUnit
|
|
value={percent}
|
|
onChange={handleChange}
|
|
onSubmit={handleSubmit}
|
|
placeholder="할인율(%) 소수점 첫째 자리까지"
|
|
unit="%"
|
|
error={error}
|
|
inputRef={inputRef}
|
|
ariaLabel="할인율 입력"
|
|
/>
|
|
)
|
|
}
|
|
|
|
export function Price({ priceErrorMessage }: { priceErrorMessage?: string }) {
|
|
const [inputValue, setInputValue] = useState('')
|
|
const [error, setError] = useState('')
|
|
const inputRef = useRef<HTMLInputElement>(null)
|
|
const sendMessage = useChatStore((s) => s.sendMessage)
|
|
const setPriceErrorMessage = useChatStore((s) => s.setPriceErrorMessage)
|
|
|
|
const displayError = priceErrorMessage || error
|
|
|
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const value = e.target.value
|
|
if (priceErrorMessage) setPriceErrorMessage('')
|
|
if (value === '') {
|
|
setInputValue('')
|
|
setError('')
|
|
return
|
|
}
|
|
const numberOnly = value.replace(/,/g, '')
|
|
if (!/^\d*$/.test(numberOnly)) return
|
|
if (parseInt(numberOnly) > 999999999999) return
|
|
setInputValue(numberOnly.replace(/\B(?=(\d{3})+(?!\d))/g, ','))
|
|
setError('')
|
|
}
|
|
|
|
const handleSubmit = () => {
|
|
if (!inputValue) {
|
|
if (priceErrorMessage) setPriceErrorMessage('')
|
|
setError('가격을 입력해주세요')
|
|
return
|
|
}
|
|
sendMessage(`${inputValue}원`, 'price')
|
|
setError('')
|
|
}
|
|
|
|
return (
|
|
<InputWithUnit
|
|
value={inputValue}
|
|
onChange={handleChange}
|
|
onSubmit={handleSubmit}
|
|
placeholder="가격(원)을 입력해주세요"
|
|
unit="원"
|
|
error={displayError}
|
|
inputRef={inputRef}
|
|
ariaLabel="가격 입력"
|
|
/>
|
|
)
|
|
}
|