- 세션 목록을 useSessionListQuery 로 서버 조회(필터·정렬·페이지 위임), mock 제거. 정수 코드↔한국어 라벨 변환 어댑터(list/lib/adapter) - 협상 참여: useParticipateMutation, 성공 시 채팅 진입·실패 토스트 - 거부: 사유 입력 팝업(RejectPopup) + useRejectMutation, 상태별 차단 안내 - 선택/상태 검증을 토스트로 일원화(ActionSection 은 버튼만 담당) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
125 lines
4.2 KiB
TypeScript
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>
|
|
)
|
|
}
|