summaryrefslogtreecommitdiff
path: root/lib/rfq-last/vendor/send-rfq-dialog.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/rfq-last/vendor/send-rfq-dialog.tsx')
-rw-r--r--lib/rfq-last/vendor/send-rfq-dialog.tsx803
1 files changed, 569 insertions, 234 deletions
diff --git a/lib/rfq-last/vendor/send-rfq-dialog.tsx b/lib/rfq-last/vendor/send-rfq-dialog.tsx
index dc420cad..9d88bdc9 100644
--- a/lib/rfq-last/vendor/send-rfq-dialog.tsx
+++ b/lib/rfq-last/vendor/send-rfq-dialog.tsx
@@ -14,8 +14,8 @@ 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 { ScrollArea } from "@/components/ui/scroll-area";
import { Checkbox } from "@/components/ui/checkbox";
+import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import {
Send,
Building2,
@@ -33,12 +33,18 @@ import {
Info,
File,
CheckCircle,
- RefreshCw
+ 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 } from "@/lib/utils";
+import { cn, formatDate } from "@/lib/utils";
import {
Tooltip,
TooltipContent,
@@ -47,16 +53,54 @@ import {
} from "@/components/ui/tooltip";
import {
Alert,
- AlertDescription,
+ 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<string, ContactDetail[]>;
+ primaryEmail?: string | null;
currency?: string | null;
}
@@ -93,7 +137,9 @@ interface RfqInfo {
}
interface VendorWithRecipients extends Vendor {
- additionalRecipients: string[];
+ selectedMainEmail: string;
+ additionalEmails: string[];
+ customEmails: CustomEmail[];
}
interface SendRfqDialogProps {
@@ -109,6 +155,12 @@ interface SendRfqDialogProps {
}) => Promise<void>;
}
+// 이메일 유효성 검사 함수
+const validateEmail = (email: string): boolean => {
+ const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ return re.test(email);
+};
+
// 첨부파일 타입별 아이콘
const getAttachmentIcon = (type: string) => {
switch (type.toLowerCase()) {
@@ -132,6 +184,40 @@ const formatFileSize = (bytes?: number) => {
return `${kb.toFixed(2)} KB`;
};
+// 포지션별 아이콘
+const getPositionIcon = (position?: string | null) => {
+ if (!position) return <User className="h-3 w-3" />;
+
+ const lowerPosition = position.toLowerCase();
+ if (lowerPosition.includes('대표') || lowerPosition.includes('ceo')) {
+ return <Building2 className="h-3 w-3" />;
+ }
+ if (lowerPosition.includes('영업') || lowerPosition.includes('sales')) {
+ return <Briefcase className="h-3 w-3" />;
+ }
+ if (lowerPosition.includes('기술') || lowerPosition.includes('tech')) {
+ return <Package className="h-3 w-3" />;
+ }
+ return <User className="h-3 w-3" />;
+};
+
+// 포지션별 색상
+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,
@@ -144,6 +230,9 @@ export function SendRfqDialog({
const [vendorsWithRecipients, setVendorsWithRecipients] = React.useState<VendorWithRecipients[]>([]);
const [selectedAttachments, setSelectedAttachments] = React.useState<number[]>([]);
const [additionalMessage, setAdditionalMessage] = React.useState("");
+ const [expandedVendors, setExpandedVendors] = React.useState<number[]>([]);
+ const [customEmailInputs, setCustomEmailInputs] = React.useState<Record<number, { email: string; name: string }>>({});
+ const [showCustomEmailForm, setShowCustomEmailForm] = React.useState<Record<number, boolean>>({});
// 초기화
React.useEffect(() => {
@@ -151,48 +240,135 @@ export function SendRfqDialog({
setVendorsWithRecipients(
selectedVendors.map(v => ({
...v,
- additionalRecipients: []
+ 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 handleAddRecipient = (vendorId: number, email: string) => {
- if (!email) return;
-
- // 이메일 유효성 검사
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
- if (!emailRegex.test(email)) {
+ // 커스텀 이메일 추가
+ 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 =>
- v.vendorId === vendorId
- ? { ...v, additionalRecipients: [...v.additionalRecipients, email] }
- : v
- )
+ 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 handleRemoveRecipient = (vendorId: number, index: number) => {
+ // 커스텀 이메일 삭제
+ 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,
- additionalRecipients: v.additionalRecipients.filter((_, i) => i !== index)
- }
+ ? { ...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 =>
@@ -206,15 +382,24 @@ export function SendRfqDialog({
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,
+ vendors: vendorsWithRecipients.map(v => ({
+ ...v,
+ additionalRecipients: v.additionalEmails,
+ })),
attachments: selectedAttachments,
message: additionalMessage,
});
@@ -231,14 +416,14 @@ export function SendRfqDialog({
// 총 수신자 수 계산
const totalRecipientCount = React.useMemo(() => {
- return vendorsWithRecipients.reduce((acc, v) =>
- acc + 1 + v.additionalRecipients.length, 0
+ return vendorsWithRecipients.reduce((acc, v) =>
+ acc + 1 + v.additionalEmails.length, 0
);
}, [vendorsWithRecipients]);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
- <DialogContent className="max-w-4xl max-h-[90vh] flex flex-col">
+ <DialogContent className="max-w-5xl max-h-[90vh] flex flex-col">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Send className="h-5 w-5" />
@@ -249,7 +434,8 @@ export function SendRfqDialog({
</DialogDescription>
</DialogHeader>
- <ScrollArea className="flex-1 max-h-[calc(90vh-200px)]">
+ {/* ScrollArea 대신 div 사용 */}
+ <div className="flex-1 overflow-y-auto px-1" style={{ maxHeight: 'calc(90vh - 200px)' }}>
<div className="space-y-6 pr-4">
{/* RFQ 정보 섹션 */}
<div className="space-y-4">
@@ -257,88 +443,367 @@ export function SendRfqDialog({
<Info className="h-4 w-4" />
RFQ 정보
</div>
-
+
<div className="bg-muted/50 rounded-lg p-4 space-y-3">
- {/* 프로젝트 정보 */}
<div className="grid grid-cols-2 gap-4 text-sm">
<div className="flex items-start gap-2">
- <span className="text-muted-foreground min-w-[80px]">프로젝트:</span>
- <span className="font-medium">
- {rfqInfo.projectCode || "PN003"} ({rfqInfo.projectName || "PETRONAS ZLNG nearshore project"})
- </span>
+ <span className="text-muted-foreground min-w-[80px]">RFQ 코드:</span>
+ <span className="font-medium">{rfqInfo?.rfqCode}</span>
</div>
<div className="flex items-start gap-2">
- <span className="text-muted-foreground min-w-[80px]">견적번호:</span>
- <span className="font-medium font-mono">{rfqInfo.rfqCode}</span>
- </div>
- </div>
-
- {/* 담당자 정보 */}
- <div className="grid grid-cols-2 gap-4 text-sm">
- <div className="flex items-start gap-2">
- <span className="text-muted-foreground min-w-[80px]">구매담당:</span>
- <span>
- {rfqInfo.picName || "김*종"} ({rfqInfo.picCode || "86D"}) {rfqInfo.picTeam || "해양구매팀(해양구매1)"}
+ <span className="text-muted-foreground min-w-[80px]">견적마감일:</span>
+ <span className="font-medium text-red-600">
+ {formatDate(rfqInfo?.dueDate, "KR")}
</span>
</div>
<div className="flex items-start gap-2">
- <span className="text-muted-foreground min-w-[80px]">설계담당:</span>
- <span>
- {rfqInfo.designPicName || "이*진"} {rfqInfo.designTeam || "전장설계팀 (전장기기시스템)"}
+ <span className="text-muted-foreground min-w-[80px]">프로젝트:</span>
+ <span className="font-medium">
+ {rfqInfo?.projectCode} ({rfqInfo?.projectName})
</span>
</div>
- </div>
-
- {/* PKG 및 자재 정보 */}
- <div className="grid grid-cols-2 gap-4 text-sm">
<div className="flex items-start gap-2">
- <span className="text-muted-foreground min-w-[80px]">PKG 정보:</span>
- <span>
- {rfqInfo.packageNo || "MM03"} ({rfqInfo.packageName || "Deck Machinery"})
+ <span className="text-muted-foreground min-w-[80px]"> 자재그룹:</span>
+ <span className="font-medium">
+ {rfqInfo?.packageNo} - {rfqInfo?.packageName}
</span>
</div>
<div className="flex items-start gap-2">
- <span className="text-muted-foreground min-w-[80px]">자재그룹:</span>
- <span>
- {rfqInfo.materialGroup || "BE2101"} ({rfqInfo.materialGroupDesc || "Combined Windlass & Mooring Wi"})
+ <span className="text-muted-foreground min-w-[80px]">구매담당자:</span>
+ <span className="font-medium">
+ {rfqInfo?.picName} ({rfqInfo?.picCode}) {rfqInfo?.picTeam}
</span>
</div>
- </div>
-
- {/* 견적 정보 */}
- <div className="grid grid-cols-2 gap-4 text-sm">
<div className="flex items-start gap-2">
- <span className="text-muted-foreground min-w-[80px]">견적마감일:</span>
- <span className="font-medium text-red-600">
- {format(rfqInfo.dueDate, "yyyy.MM.dd", { locale: ko })}
+ <span className="text-muted-foreground min-w-[80px]"> 설계담당자:</span>
+ <span className="font-medium">
+ {rfqInfo?.designPicName}
</span>
</div>
- <div className="flex items-start gap-2">
- <span className="text-muted-foreground min-w-[80px]">평가적용:</span>
- <Badge variant={rfqInfo.evaluationApply ? "default" : "outline"}>
- {rfqInfo.evaluationApply ? "Y" : "N"}
- </Badge>
- </div>
+ {rfqInfo?.rfqCode.startsWith("F") &&
+ <>
+ <div className="flex items-start gap-2">
+ <span className="text-muted-foreground min-w-[80px]">견적명:</span>
+ <span className="font-medium">
+ {rfqInfo?.rfqTitle}
+ </span>
+ </div>
+ <div className="flex items-start gap-2">
+ <span className="text-muted-foreground min-w-[80px]"> 견적종류:</span>
+ <span className="font-medium">
+ {rfqInfo?.rfqType}
+ </span>
+ </div>
+ </>
+ }
</div>
+ </div>
+ </div>
- {/* 견적명 */}
- <div className="flex items-start gap-2 text-sm">
- <span className="text-muted-foreground min-w-[80px]">견적명:</span>
- <span className="font-medium">{rfqInfo.rfqTitle}</span>
+ <Separator />
+
+ {/* 수신 업체 섹션 - 테이블 버전 with 인라인 추가 폼 */}
+ <div className="space-y-4">
+ <div className="flex items-center justify-between">
+ <div className="flex items-center gap-2 text-sm font-medium">
+ <Building2 className="h-4 w-4" />
+ 수신 업체 ({selectedVendors.length})
</div>
+ <Badge variant="outline" className="flex items-center gap-1">
+ <Users className="h-3 w-3" />
+ 총 {totalRecipientCount}명
+ </Badge>
+ </div>
- {/* 계약구분 (일반견적일 때만) */}
- {rfqInfo.rfqType === "일반견적" && (
- <div className="flex items-start gap-2 text-sm">
- <span className="text-muted-foreground min-w-[80px]">계약구분:</span>
- <span>{rfqInfo.contractType || "-"}</span>
- </div>
- )}
+ <div className="border rounded-lg overflow-hidden">
+ <table className="w-full">
+ <thead className="bg-muted/50 border-b">
+ <tr>
+ <th className="text-left p-2 text-xs font-medium">No.</th>
+ <th className="text-left p-2 text-xs font-medium">업체명</th>
+ <th className="text-left p-2 text-xs font-medium">주 수신자</th>
+ <th className="text-left p-2 text-xs font-medium">CC</th>
+ <th className="text-left p-2 text-xs font-medium">작업</th>
+ </tr>
+ </thead>
+ <tbody>
+ {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 (
+ <React.Fragment key={vendor.vendorId}>
+ <tr className="border-b hover:bg-muted/20">
+ <td className="p-2">
+ <div className="flex items-center justify-center w-5 h-5 rounded-full bg-primary/10 text-primary text-xs font-medium">
+ {index + 1}
+ </div>
+ </td>
+ <td className="p-2">
+ <div className="space-y-1">
+ <div className="font-medium text-sm">{vendor.vendorName}</div>
+ <div className="flex items-center gap-1">
+ <Badge variant="outline" className="text-xs">
+ {vendor.vendorCountry}
+ </Badge>
+ <span className="text-xs text-muted-foreground">
+ {vendor.vendorCode}
+ </span>
+ </div>
+ </div>
+ </td>
+ <td className="p-2">
+ <Select
+ value={vendor.selectedMainEmail}
+ onValueChange={(value) => handleMainEmailChange(vendor.vendorId, value)}
+ >
+ <SelectTrigger className="h-7 text-xs w-[200px]">
+ <SelectValue placeholder="선택하세요">
+ {selectedMainEmailInfo && (
+ <div className="flex items-center gap-1">
+ {selectedMainEmailInfo.type === 'representative' && <Building2 className="h-3 w-3" />}
+ {selectedMainEmailInfo.type === 'custom' && <UserPlus className="h-3 w-3 text-green-500" />}
+ <span className="truncate">{selectedMainEmailInfo.label}</span>
+ </div>
+ )}
+ </SelectValue>
+ </SelectTrigger>
+ <SelectContent>
+ {allEmails.map((email) => (
+ <SelectItem key={email.value} value={email.value} className="text-xs">
+ <div className="flex items-center gap-1">
+ {email.type === 'representative' && <Building2 className="h-3 w-3" />}
+ {email.type === 'custom' && <UserPlus className="h-3 w-3 text-green-500" />}
+ <span>{email.label}</span>
+ </div>
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ {!vendor.selectedMainEmail && (
+ <span className="text-xs text-red-500">필수</span>
+ )}
+ </td>
+ <td className="p-2">
+ <Popover>
+ <PopoverTrigger asChild>
+ <Button variant="outline" className="h-7 text-xs">
+ {vendor.additionalEmails.length > 0
+ ? `${vendor.additionalEmails.length}명`
+ : "선택"
+ }
+ <ChevronDown className="ml-1 h-3 w-3" />
+ </Button>
+ </PopoverTrigger>
+ <PopoverContent className="w-48 p-2">
+ <div className="max-h-48 overflow-y-auto space-y-1">
+ {ccEmails.map((email) => (
+ <div key={email.value} className="flex items-center space-x-1 p-1">
+ <Checkbox
+ checked={vendor.additionalEmails.includes(email.value)}
+ onCheckedChange={() => toggleAdditionalEmail(vendor.vendorId, email.value)}
+ className="h-3 w-3"
+ />
+ <label className="text-xs cursor-pointer flex-1 truncate">
+ {email.label}
+ </label>
+ </div>
+ ))}
+ </div>
+ </PopoverContent>
+ </Popover>
+ </td>
+ <td className="p-2">
+ <div className="flex items-center gap-1">
+ <TooltipProvider>
+ <Tooltip>
+ <TooltipTrigger asChild>
+ <Button
+ variant={isFormOpen ? "default" : "ghost"}
+ size="sm"
+ className="h-6 w-6 p-0"
+ onClick={() => {
+ setShowCustomEmailForm(prev => ({
+ ...prev,
+ [vendor.vendorId]: !prev[vendor.vendorId]
+ }));
+ }}
+ >
+ {isFormOpen ? <X className="h-3 w-3" /> : <Plus className="h-3 w-3" />}
+ </Button>
+ </TooltipTrigger>
+ <TooltipContent>{isFormOpen ? "닫기" : "수신자 추가"}</TooltipContent>
+ </Tooltip>
+ </TooltipProvider>
+ {vendor.customEmails.length > 0 && (
+ <Badge variant="success" className="text-xs">
+ +{vendor.customEmails.length}
+ </Badge>
+ )}
+ </div>
+ </td>
+ </tr>
+
+ {/* 인라인 수신자 추가 폼 - 한 줄 레이아웃 */}
+ {isFormOpen && (
+ <tr className="bg-muted/10 border-b">
+ <td colSpan={5} className="p-4">
+ <div className="space-y-3">
+ <div className="flex items-center justify-between mb-2">
+ <div className="flex items-center gap-2 text-sm font-medium">
+ <UserPlus className="h-4 w-4" />
+ 수신자 추가 - {vendor.vendorName}
+ </div>
+ <Button
+ variant="ghost"
+ size="sm"
+ className="h-6 w-6 p-0"
+ onClick={() => setShowCustomEmailForm(prev => ({
+ ...prev,
+ [vendor.vendorId]: false
+ }))}
+ >
+ <X className="h-3 w-3" />
+ </Button>
+ </div>
+
+ {/* 한 줄에 모든 요소 배치 - 명확한 너비 지정 */}
+ <div className="flex gap-2 items-end">
+ <div className="w-[150px]">
+ <Label className="text-xs mb-1 block">이름 (선택)</Label>
+ <Input
+ placeholder="홍길동"
+ className="h-8 text-sm"
+ value={customEmailInputs[vendor.vendorId]?.name || ''}
+ onChange={(e) => setCustomEmailInputs(prev => ({
+ ...prev,
+ [vendor.vendorId]: {
+ ...prev[vendor.vendorId],
+ name: e.target.value
+ }
+ }))}
+ />
+ </div>
+ <div className="flex-1">
+ <Label className="text-xs mb-1 block">이메일 <span className="text-red-500">*</span></Label>
+ <Input
+ type="email"
+ placeholder="example@company.com"
+ className="h-8 text-sm"
+ value={customEmailInputs[vendor.vendorId]?.email || ''}
+ onChange={(e) => setCustomEmailInputs(prev => ({
+ ...prev,
+ [vendor.vendorId]: {
+ ...prev[vendor.vendorId],
+ email: e.target.value
+ }
+ }))}
+ onKeyPress={(e) => {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ addCustomEmail(vendor.vendorId);
+ }
+ }}
+ />
+ </div>
+ <Button
+ size="sm"
+ className="h-8 px-4"
+ onClick={() => addCustomEmail(vendor.vendorId)}
+ disabled={!customEmailInputs[vendor.vendorId]?.email}
+ >
+ <Plus className="h-3 w-3 mr-1" />
+ 추가
+ </Button>
+ <Button
+ variant="outline"
+ size="sm"
+ className="h-8 px-4"
+ onClick={() => {
+ setCustomEmailInputs(prev => ({
+ ...prev,
+ [vendor.vendorId]: { email: '', name: '' }
+ }));
+ setShowCustomEmailForm(prev => ({
+ ...prev,
+ [vendor.vendorId]: false
+ }));
+ }}
+ >
+ 취소
+ </Button>
+ </div>
+
+ {/* 추가된 커스텀 이메일 목록 */}
+ {vendor.customEmails.length > 0 && (
+ <div className="mt-3 pt-3 border-t">
+ <div className="text-xs text-muted-foreground mb-2">추가된 수신자 목록</div>
+ <div className="grid grid-cols-2 xl:grid-cols-3 gap-2">
+ {vendor.customEmails.map((custom) => (
+ <div key={custom.id} className="flex items-center justify-between bg-background rounded-md p-2">
+ <div className="flex items-center gap-2 min-w-0">
+ <UserPlus className="h-3 w-3 text-green-500 flex-shrink-0" />
+ <div className="min-w-0">
+ <div className="text-sm font-medium truncate">{custom.name}</div>
+ <div className="text-xs text-muted-foreground truncate">{custom.email}</div>
+ </div>
+ </div>
+ <Button
+ variant="ghost"
+ size="sm"
+ className="h-6 w-6 p-0 flex-shrink-0"
+ onClick={() => removeCustomEmail(vendor.vendorId, custom.id)}
+ >
+ <X className="h-3 w-3" />
+ </Button>
+ </div>
+ ))}
+ </div>
+ </div>
+ )}
+ </div>
+ </td>
+ </tr>
+ )}
+ </React.Fragment>
+ );
+ })}
+ </tbody>
+ </table>
</div>
</div>
- <Separator />
+ <Separator />
{/* 첨부파일 섹션 */}
<div className="space-y-4">
<div className="flex items-center justify-between">
@@ -361,45 +826,30 @@ export function SendRfqDialog({
</Button>
</div>
- <div className="border rounded-lg divide-y">
+ <div className="border rounded-lg divide-y max-h-40 overflow-y-auto">
{attachments.length > 0 ? (
attachments.map((attachment) => (
<div
key={attachment.id}
- className="flex items-center justify-between p-3 hover:bg-muted/50 transition-colors"
+ className="flex items-center justify-between p-2 hover:bg-muted/50 transition-colors"
>
- <div className="flex items-center gap-3">
+ <div className="flex items-center gap-2">
<Checkbox
checked={selectedAttachments.includes(attachment.id)}
onCheckedChange={() => toggleAttachment(attachment.id)}
/>
{getAttachmentIcon(attachment.attachmentType)}
- <div>
- <div className="flex items-center gap-2">
- <span className="text-sm font-medium">
- {attachment.fileName || `${attachment.attachmentType}_${attachment.serialNo}`}
- </span>
- <Badge variant="outline" className="text-xs">
- {attachment.currentRevision}
- </Badge>
- </div>
- {attachment.description && (
- <p className="text-xs text-muted-foreground mt-0.5">
- {attachment.description}
- </p>
- )}
- </div>
- </div>
- <div className="flex items-center gap-2">
- <span className="text-xs text-muted-foreground">
- {formatFileSize(attachment.fileSize)}
+ <span className="text-sm">
+ {attachment.fileName || `${attachment.attachmentType}_${attachment.serialNo}`}
</span>
+ <Badge variant="outline" className="text-xs">
+ {attachment.currentRevision}
+ </Badge>
</div>
</div>
))
) : (
- <div className="p-8 text-center text-muted-foreground">
- <Paperclip className="h-8 w-8 mx-auto mb-2 opacity-50" />
+ <div className="p-4 text-center text-muted-foreground">
<p className="text-sm">첨부파일이 없습니다.</p>
</div>
)}
@@ -408,124 +858,7 @@ export function SendRfqDialog({
<Separator />
- {/* 수신 업체 섹션 */}
- <div className="space-y-4">
- <div className="flex items-center justify-between">
- <div className="flex items-center gap-2 text-sm font-medium">
- <Building2 className="h-4 w-4" />
- 수신 업체 ({selectedVendors.length})
- </div>
- <Badge variant="outline" className="flex items-center gap-1">
- <Users className="h-3 w-3" />
- 총 {totalRecipientCount}명
- </Badge>
- </div>
-
- <div className="space-y-3">
- {vendorsWithRecipients.map((vendor, index) => (
- <div
- key={vendor.vendorId}
- className="border rounded-lg p-4 space-y-3"
- >
- {/* 업체 정보 */}
- <div className="flex items-start justify-between">
- <div className="flex items-center gap-3">
- <div className="flex items-center justify-center w-8 h-8 rounded-full bg-primary/10 text-primary text-sm font-medium">
- {index + 1}
- </div>
- <div>
- <div className="flex items-center gap-2">
- <span className="font-medium">{vendor.vendorName}</span>
- <Badge variant="outline" className="text-xs">
- {vendor.vendorCountry}
- </Badge>
- </div>
- {vendor.vendorCode && (
- <span className="text-xs text-muted-foreground">
- {vendor.vendorCode}
- </span>
- )}
- </div>
- </div>
- <Badge variant="secondary">
- 주 수신: {vendor.vendorEmail || "vendor@example.com"}
- </Badge>
- </div>
-
- {/* 추가 수신처 */}
- <div className="pl-11 space-y-2">
- <div className="flex items-center gap-2">
- <Label className="text-xs text-muted-foreground">추가 수신처:</Label>
- <TooltipProvider>
- <Tooltip>
- <TooltipTrigger>
- <AlertCircle className="h-3 w-3 text-muted-foreground" />
- </TooltipTrigger>
- <TooltipContent>
- <p>참조로 RFQ를 받을 추가 이메일 주소를 입력하세요.</p>
- </TooltipContent>
- </Tooltip>
- </TooltipProvider>
- </div>
-
- {/* 추가된 이메일 목록 */}
- <div className="flex flex-wrap gap-2">
- {vendor.additionalRecipients.map((email, idx) => (
- <Badge
- key={idx}
- variant="outline"
- className="flex items-center gap-1 pr-1"
- >
- <Mail className="h-3 w-3" />
- {email}
- <Button
- variant="ghost"
- size="sm"
- className="h-4 w-4 p-0 hover:bg-transparent"
- onClick={() => handleRemoveRecipient(vendor.vendorId, idx)}
- >
- <X className="h-3 w-3" />
- </Button>
- </Badge>
- ))}
- </div>
-
- {/* 이메일 입력 필드 */}
- <div className="flex gap-2">
- <Input
- type="email"
- placeholder="추가 수신자 이메일 입력"
- className="h-8 text-sm"
- onKeyDown={(e) => {
- if (e.key === "Enter") {
- e.preventDefault();
- const input = e.target as HTMLInputElement;
- handleAddRecipient(vendor.vendorId, input.value);
- input.value = "";
- }
- }}
- />
- <Button
- variant="outline"
- size="sm"
- onClick={(e) => {
- const input = (e.currentTarget.previousElementSibling as HTMLInputElement);
- handleAddRecipient(vendor.vendorId, input.value);
- input.value = "";
- }}
- >
- <Plus className="h-4 w-4" />
- </Button>
- </div>
- </div>
- </div>
- ))}
- </div>
- </div>
-
- <Separator />
-
- {/* 추가 메시지 (선택사항) */}
+ {/* 추가 메시지 */}
<div className="space-y-2">
<Label htmlFor="message" className="text-sm font-medium">
추가 메시지 (선택사항)
@@ -539,14 +872,16 @@ export function SendRfqDialog({
/>
</div>
</div>
- </ScrollArea>
+ </div>
<DialogFooter className="flex-shrink-0">
- <Alert className="mr-auto max-w-md">
- <AlertCircle className="h-4 w-4" />
- <AlertDescription className="text-xs">
- 발송 후에는 취소할 수 없습니다. 발송 내용을 다시 한번 확인해주세요.
- </AlertDescription>
+ <Alert className="max-w-md">
+ <div className="flex items-center gap-2">
+ <AlertCircle className="h-4 w-4 flex-shrink-0" />
+ <AlertDescription className="text-xs">
+ 발송 후에는 취소할 수 없습니다. 발송 내용을 다시 한번 확인해주세요.
+ </AlertDescription>
+ </div>
</Alert>
<Button
variant="outline"