"use client"; import * as React from "react"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Separator } from "@/components/ui/separator"; import { Checkbox } from "@/components/ui/checkbox"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Send, Building2, User, Calendar, Package, FileText, Plus, X, Paperclip, Download, Mail, Users, AlertCircle, Info, File, CheckCircle, RefreshCw, Phone, Briefcase, Building, ChevronDown, ChevronRight, UserPlus } from "lucide-react"; import { format } from "date-fns"; import { ko } from "date-fns/locale"; import { toast } from "sonner"; import { cn, formatDate } from "@/lib/utils"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible"; import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue, } from "@/components/ui/select" import { Popover, PopoverTrigger, PopoverContent, } from "@/components/ui/popover" // 타입 정의 interface ContactDetail { id: number; name: string; position?: string | null; department?: string | null; email: string; phone?: string | null; isPrimary: boolean; } interface CustomEmail { id: string; email: string; name?: string; } interface Vendor { vendorId: number; vendorName: string; vendorCode?: string | null; vendorCountry?: string | null; vendorEmail?: string | null; representativeEmail?: string | null; contacts?: ContactDetail[]; contactsByPosition?: Record; primaryEmail?: string | null; currency?: string | null; } interface Attachment { id: number; attachmentType: string; serialNo: string; currentRevision: string; description?: string; fileName?: string; fileSize?: number; uploadedAt?: Date; } interface RfqInfo { rfqCode: string; rfqTitle: string; rfqType: string; projectCode?: string; projectName?: string; picName?: string; picCode?: string; picTeam?: string; packageNo?: string; packageName?: string; designPicName?: string; designTeam?: string; materialGroup?: string; materialGroupDesc?: string; dueDate: Date; quotationType?: string; evaluationApply?: boolean; contractType?: string; } interface VendorWithRecipients extends Vendor { selectedMainEmail: string; additionalEmails: string[]; customEmails: CustomEmail[]; } interface SendRfqDialogProps { open: boolean; onOpenChange: (open: boolean) => void; selectedVendors: Vendor[]; rfqInfo: RfqInfo; attachments?: Attachment[]; onSend: (data: { vendors: VendorWithRecipients[]; attachments: number[]; message?: string; }) => Promise; } // 이메일 유효성 검사 함수 const validateEmail = (email: string): boolean => { const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return re.test(email); }; // 첨부파일 타입별 아이콘 const getAttachmentIcon = (type: string) => { switch (type.toLowerCase()) { case "technical": return ; case "commercial": return ; case "drawing": return ; default: return ; } }; // 파일 크기 포맷 const formatFileSize = (bytes?: number) => { if (!bytes) return "0 KB"; const kb = bytes / 1024; const mb = kb / 1024; if (mb >= 1) return `${mb.toFixed(2)} MB`; return `${kb.toFixed(2)} KB`; }; // 포지션별 아이콘 const getPositionIcon = (position?: string | null) => { if (!position) return ; const lowerPosition = position.toLowerCase(); if (lowerPosition.includes('대표') || lowerPosition.includes('ceo')) { return ; } if (lowerPosition.includes('영업') || lowerPosition.includes('sales')) { return ; } if (lowerPosition.includes('기술') || lowerPosition.includes('tech')) { return ; } return ; }; // 포지션별 색상 const getPositionColor = (position?: string | null) => { if (!position) return 'default'; const lowerPosition = position.toLowerCase(); if (lowerPosition.includes('대표') || lowerPosition.includes('ceo')) { return 'destructive'; } if (lowerPosition.includes('영업') || lowerPosition.includes('sales')) { return 'success'; } if (lowerPosition.includes('기술') || lowerPosition.includes('tech')) { return 'secondary'; } return 'default'; }; export function SendRfqDialog({ open, onOpenChange, selectedVendors, rfqInfo, attachments = [], onSend, }: SendRfqDialogProps) { const [isSending, setIsSending] = React.useState(false); const [vendorsWithRecipients, setVendorsWithRecipients] = React.useState([]); const [selectedAttachments, setSelectedAttachments] = React.useState([]); const [additionalMessage, setAdditionalMessage] = React.useState(""); const [expandedVendors, setExpandedVendors] = React.useState([]); const [customEmailInputs, setCustomEmailInputs] = React.useState>({}); const [showCustomEmailForm, setShowCustomEmailForm] = React.useState>({}); // 초기화 React.useEffect(() => { if (open && selectedVendors.length > 0) { setVendorsWithRecipients( selectedVendors.map(v => ({ ...v, selectedMainEmail: v.primaryEmail || v.vendorEmail || '', additionalEmails: [], customEmails: [] })) ); // 모든 첨부파일 선택 setSelectedAttachments(attachments.map(a => a.id)); // 첫 번째 벤더를 자동으로 확장 if (selectedVendors.length > 0) { setExpandedVendors([selectedVendors[0].vendorId]); } // 초기화 setCustomEmailInputs({}); setShowCustomEmailForm({}); } }, [open, selectedVendors, attachments]); // 커스텀 이메일 추가 const addCustomEmail = (vendorId: number) => { const input = customEmailInputs[vendorId]; if (!input || !input.email) { toast.error("이메일 주소를 입력해주세요."); return; } if (!validateEmail(input.email)) { toast.error("올바른 이메일 형식이 아닙니다."); return; } setVendorsWithRecipients(prev => prev.map(v => { if (v.vendorId !== vendorId) return v; // 중복 체크 const allEmails = [ v.vendorEmail, v.representativeEmail, ...(v.contacts?.map(c => c.email) || []), ...v.customEmails.map(c => c.email) ].filter(Boolean); if (allEmails.includes(input.email)) { toast.error("이미 등록된 이메일 주소입니다."); return v; } const newCustomEmail: CustomEmail = { id: `custom-${Date.now()}`, email: input.email, name: input.name || input.email.split('@')[0] }; return { ...v, customEmails: [...v.customEmails, newCustomEmail] }; }) ); // 입력 필드 초기화 setCustomEmailInputs(prev => ({ ...prev, [vendorId]: { email: '', name: '' } })); setShowCustomEmailForm(prev => ({ ...prev, [vendorId]: false })); toast.success("수신자가 추가되었습니다."); }; // 커스텀 이메일 삭제 const removeCustomEmail = (vendorId: number, emailId: string) => { setVendorsWithRecipients(prev => prev.map(v => { if (v.vendorId !== vendorId) return v; const emailToRemove = v.customEmails.find(e => e.id === emailId); if (!emailToRemove) return v; return { ...v, customEmails: v.customEmails.filter(e => e.id !== emailId), // 만약 삭제하는 이메일이 선택된 주 수신자라면 초기화 selectedMainEmail: v.selectedMainEmail === emailToRemove.email ? '' : v.selectedMainEmail, // 추가 수신자에서도 제거 additionalEmails: v.additionalEmails.filter(e => e !== emailToRemove.email) }; }) ); }; // 주 수신자 이메일 변경 const handleMainEmailChange = (vendorId: number, email: string) => { setVendorsWithRecipients(prev => prev.map(v => v.vendorId === vendorId ? { ...v, selectedMainEmail: email } : v ) ); }; // 추가 수신자 토글 const toggleAdditionalEmail = (vendorId: number, email: string) => { setVendorsWithRecipients(prev => prev.map(v => { if (v.vendorId !== vendorId) return v; const additionalEmails = v.additionalEmails.includes(email) ? v.additionalEmails.filter(e => e !== email) : [...v.additionalEmails, email]; return { ...v, additionalEmails }; }) ); }; // 벤더 확장/축소 토글 const toggleVendorExpand = (vendorId: number) => { setExpandedVendors(prev => prev.includes(vendorId) ? prev.filter(id => id !== vendorId) : [...prev, vendorId] ); }; // 첨부파일 선택 토글 const toggleAttachment = (attachmentId: number) => { setSelectedAttachments(prev => prev.includes(attachmentId) ? prev.filter(id => id !== attachmentId) : [...prev, attachmentId] ); }; // 전송 처리 const handleSend = async () => { try { setIsSending(true); // 유효성 검사 const vendorsWithoutEmail = vendorsWithRecipients.filter(v => !v.selectedMainEmail); if (vendorsWithoutEmail.length > 0) { toast.error(`${vendorsWithoutEmail.map(v => v.vendorName).join(', ')}의 주 수신자를 선택해주세요.`); return; } if (selectedAttachments.length === 0) { toast.warning("최소 하나 이상의 첨부파일을 선택해주세요."); return; } await onSend({ vendors: vendorsWithRecipients.map(v => ({ ...v, additionalRecipients: v.additionalEmails, })), attachments: selectedAttachments, message: additionalMessage, }); toast.success(`${vendorsWithRecipients.length}개 업체에 RFQ를 발송했습니다.`); onOpenChange(false); } catch (error) { console.error("RFQ 발송 실패:", error); toast.error("RFQ 발송에 실패했습니다."); } finally { setIsSending(false); } }; // 총 수신자 수 계산 const totalRecipientCount = React.useMemo(() => { return vendorsWithRecipients.reduce((acc, v) => acc + 1 + v.additionalEmails.length, 0 ); }, [vendorsWithRecipients]); return ( RFQ 일괄 발송 선택한 {selectedVendors.length}개 업체에 RFQ를 발송합니다. {/* ScrollArea 대신 div 사용 */}
{/* RFQ 정보 섹션 */}
RFQ 정보
RFQ 코드: {rfqInfo?.rfqCode}
견적마감일: {formatDate(rfqInfo?.dueDate, "KR")}
프로젝트: {rfqInfo?.projectCode} ({rfqInfo?.projectName})
자재그룹: {rfqInfo?.packageNo} - {rfqInfo?.packageName}
구매담당자: {rfqInfo?.picName} ({rfqInfo?.picCode}) {rfqInfo?.picTeam}
설계담당자: {rfqInfo?.designPicName}
{rfqInfo?.rfqCode.startsWith("F") && <>
견적명: {rfqInfo?.rfqTitle}
견적종류: {rfqInfo?.rfqType}
}
{/* 수신 업체 섹션 - 테이블 버전 with 인라인 추가 폼 */}
수신 업체 ({selectedVendors.length})
총 {totalRecipientCount}명
{vendorsWithRecipients.map((vendor, index) => { const allContacts = vendor.contacts || []; const allEmails = [ ...(vendor.representativeEmail ? [{ value: vendor.representativeEmail, label: '대표자', email: vendor.representativeEmail, type: 'representative' }] : []), ...allContacts.map(c => ({ value: c.email, label: `${c.name} ${c.position ? `(${c.position})` : ''}`, email: c.email, type: 'contact' })), ...vendor.customEmails.map(c => ({ value: c.email, label: c.name || c.email, email: c.email, type: 'custom' })), ...(vendor.vendorEmail && vendor.vendorEmail !== vendor.representativeEmail ? [{ value: vendor.vendorEmail, label: '업체 기본', email: vendor.vendorEmail, type: 'default' }] : []) ]; const ccEmails = allEmails.filter(e => e.value !== vendor.selectedMainEmail); const selectedMainEmailInfo = allEmails.find(e => e.value === vendor.selectedMainEmail); const isFormOpen = showCustomEmailForm[vendor.vendorId]; return ( {/* 인라인 수신자 추가 폼 - 한 줄 레이아웃 */} {isFormOpen && ( )} ); })}
No. 업체명 주 수신자 CC 작업
{index + 1}
{vendor.vendorName}
{vendor.vendorCountry} {vendor.vendorCode}
{!vendor.selectedMainEmail && ( 필수 )}
{ccEmails.map((email) => (
toggleAdditionalEmail(vendor.vendorId, email.value)} className="h-3 w-3" />
))}
{isFormOpen ? "닫기" : "수신자 추가"} {vendor.customEmails.length > 0 && ( +{vendor.customEmails.length} )}
수신자 추가 - {vendor.vendorName}
{/* 한 줄에 모든 요소 배치 - 명확한 너비 지정 */}
setCustomEmailInputs(prev => ({ ...prev, [vendor.vendorId]: { ...prev[vendor.vendorId], name: e.target.value } }))} />
setCustomEmailInputs(prev => ({ ...prev, [vendor.vendorId]: { ...prev[vendor.vendorId], email: e.target.value } }))} onKeyPress={(e) => { if (e.key === 'Enter') { e.preventDefault(); addCustomEmail(vendor.vendorId); } }} />
{/* 추가된 커스텀 이메일 목록 */} {vendor.customEmails.length > 0 && (
추가된 수신자 목록
{vendor.customEmails.map((custom) => (
{custom.name}
{custom.email}
))}
)}
{/* 첨부파일 섹션 */}
첨부파일 ({selectedAttachments.length}/{attachments.length})
{attachments.length > 0 ? ( attachments.map((attachment) => (
toggleAttachment(attachment.id)} /> {getAttachmentIcon(attachment.attachmentType)} {attachment.fileName || `${attachment.attachmentType}_${attachment.serialNo}`} {attachment.currentRevision}
)) ) : (

첨부파일이 없습니다.

)}
{/* 추가 메시지 */}