o2o-negosium-original/frontend/src/features/chat/components/userInputs.tsx
민헌 482c8ed97c feat(chat): 반응형 레이아웃(~1024px) + 리드타임 "일" 표시 + 완료세션 인디케이터 숨김
- 반응형: 최소 폭 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>
2026-07-06 16:37:18 +09:00

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="가격 입력"
/>
)
}