o2o-negosium-original/frontend/src/features/list/components/RejectPopup.tsx
민헌 fab8e5aa53 feat(frontend): 협상 목록/참여/거부 연동
- 세션 목록을 useSessionListQuery 로 서버 조회(필터·정렬·페이지 위임),
  mock 제거. 정수 코드↔한국어 라벨 변환 어댑터(list/lib/adapter)
- 협상 참여: useParticipateMutation, 성공 시 채팅 진입·실패 토스트
- 거부: 사유 입력 팝업(RejectPopup) + useRejectMutation, 상태별 차단 안내
- 선택/상태 검증을 토스트로 일원화(ActionSection 은 버튼만 담당)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 13:12:31 +09:00

125 lines
4.2 KiB
TypeScript

import { useState } from 'react'
import { Modal } from '@/components'
import { cn, interactive } from '@/lib'
const REASONS = ['단종', '품절', '기타'] as const
export interface RejectPopupProps {
onClose: () => void
/** 최종 거부 사유 (프리셋 라벨 또는 기타 입력 텍스트) */
onSubmit: (reason: string) => void
}
// 거부 사유 입력 팝업 (단종/품절/기타).
export function RejectPopup({ onClose, onSubmit }: RejectPopupProps) {
const [selectedReason, setSelectedReason] = useState<string | null>(null)
const [customReason, setCustomReason] = useState('')
const [showError, setShowError] = useState(false)
const isEtcOpen = selectedReason === '기타'
const isSubmitDisabled = !selectedReason || (isEtcOpen && !customReason.trim())
const handleReasonClick = (reason: string) => {
setShowError(false)
if (selectedReason === reason) {
setSelectedReason(null)
setCustomReason('')
} else {
setSelectedReason(reason)
if (reason !== '기타') setCustomReason('')
}
}
const handleSubmit = () => {
if (isEtcOpen && !customReason.trim()) {
setShowError(true)
return
}
if (isSubmitDisabled || !selectedReason) return
const reason = isEtcOpen ? customReason.trim() : selectedReason
onSubmit(reason)
onClose()
}
return (
<Modal onClose={onClose}>
<div className="flex h-[500px] w-[700px] flex-col items-center justify-between rounded-[10px] bg-background py-20 shadow-lg transition-all duration-300 ease-out">
{/* 헤더 */}
<p className="text-center text-[26px] font-bold leading-9 tracking-[-0.52px] text-foreground">
거부 사유를 입력해주세요
</p>
{/* 사유 선택 */}
<div className={cn('flex gap-5 transition-all duration-300 ease-out', isEtcOpen ? 'mt-0' : 'mt-12')}>
{REASONS.map((reason) => (
<button
key={reason}
type="button"
onClick={() => handleReasonClick(reason)}
className={cn(
'flex h-[50px] w-[120px] items-center justify-center rounded-full px-8 title-2 text-foreground',
'bg-neutral-20 border',
interactive,
selectedReason === reason ? 'border-foreground' : 'border-transparent',
)}
>
{reason}
</button>
))}
</div>
{/* 기타 입력 + 에러 */}
<div className="flex flex-col items-center">
<div
className={cn(
'flex w-[400px] overflow-hidden transition-all duration-300 ease-out',
isEtcOpen ? 'visible h-[120px] opacity-100' : 'invisible h-0 opacity-0',
)}
>
<textarea
placeholder="사유를 입력하여 주십시오"
value={customReason}
onChange={(e) => {
setCustomReason(e.target.value)
setShowError(false)
}}
className={cn(
'h-full w-full resize-none rounded-lg bg-background p-2.5 body-3 text-foreground',
'placeholder:text-muted-foreground focus:outline-none',
'transition-colors duration-200',
showError ? 'border-2 border-destructive' : 'border border-foreground',
)}
/>
</div>
{/* 에러 메시지 (항상 공간 확보) */}
<div
className={cn(
'w-[400px] overflow-hidden transition-all duration-300 ease-out',
showError && isEtcOpen ? 'mt-2 h-[24px] opacity-100' : 'h-0 opacity-0',
)}
>
<p className="text-left body-5 text-destructive">기타 사유를 입력해주세요</p>
</div>
</div>
{/* 전송 */}
<button
type="button"
onClick={handleSubmit}
disabled={isSubmitDisabled}
className={cn(
'flex h-[50px] w-full max-w-[130px] items-center justify-center rounded-full px-8 title-2',
'bg-foreground text-background',
interactive,
isSubmitDisabled && 'cursor-not-allowed opacity-40',
)}
>
전송
</button>
</div>
</Modal>
)
}