diff options
Diffstat (limited to 'lib/rfq-last/vendor/rfq-vendor-table.tsx')
| -rw-r--r-- | lib/rfq-last/vendor/rfq-vendor-table.tsx | 205 |
1 files changed, 147 insertions, 58 deletions
diff --git a/lib/rfq-last/vendor/rfq-vendor-table.tsx b/lib/rfq-last/vendor/rfq-vendor-table.tsx index 7f7afe14..b2ea7588 100644 --- a/lib/rfq-last/vendor/rfq-vendor-table.tsx +++ b/lib/rfq-last/vendor/rfq-vendor-table.tsx @@ -24,7 +24,8 @@ import { Globe, Package, MapPin, - Info + Info, + Loader2 } from "lucide-react"; import { format } from "date-fns"; import { ko } from "date-fns/locale"; @@ -53,6 +54,12 @@ import { BatchUpdateConditionsDialog } from "./batch-update-conditions-dialog"; import { SendRfqDialog } from "./send-rfq-dialog"; // import { VendorDetailDialog } from "./vendor-detail-dialog"; // import { sendRfqToVendors } from "@/app/actions/rfq/send-rfq.action"; +import { + getRfqSendData, + getSelectedVendorsWithEmails, + type RfqSendData, + type VendorEmailInfo +} from "../service" // 타입 정의 interface RfqDetail { @@ -100,13 +107,12 @@ interface VendorResponse { attachmentCount?: number; } -// Props 타입 정의 (중복 제거하고 하나로 통합) +// Props 타입 정의 interface RfqVendorTableProps { rfqId: number; rfqCode?: string; rfqDetails: RfqDetail[]; vendorResponses: VendorResponse[]; - // 추가 props rfqInfo?: { rfqTitle: string; rfqType: string; @@ -201,6 +207,17 @@ export function RfqVendorTable({ const [isBatchUpdateOpen, setIsBatchUpdateOpen] = React.useState(false); const [selectedVendor, setSelectedVendor] = React.useState<any | null>(null); const [isSendDialogOpen, setIsSendDialogOpen] = React.useState(false); + const [isLoadingSendData, setIsLoadingSendData] = React.useState(false); + + const [sendDialogData, setSendDialogData] = React.useState<{ + rfqInfo: RfqSendData['rfqInfo'] | null; + attachments: RfqSendData['attachments']; + selectedVendors: VendorEmailInfo[]; + }>({ + rfqInfo: null, + attachments: [], + selectedVendors: [], + }); // 데이터 병합 const mergedData = React.useMemo( @@ -215,9 +232,63 @@ export function RfqVendorTable({ return; } - // 다이얼로그 열기 - setIsSendDialogOpen(true); - }, [selectedRows]); + try { + setIsLoadingSendData(true); + + // 선택된 벤더 ID들 추출 + const selectedVendorIds = selectedRows + .map(row => row.vendorId) + .filter(id => id != null); + + if (selectedVendorIds.length === 0) { + toast.error("유효한 벤더가 선택되지 않았습니다."); + return; + } + + // 병렬로 데이터 가져오기 (에러 처리 포함) + const [rfqSendData, vendorEmailInfos] = await Promise.all([ + getRfqSendData(rfqId), + getSelectedVendorsWithEmails(rfqId, selectedVendorIds) + ]); + + // 데이터 검증 + if (!rfqSendData?.rfqInfo) { + toast.error("RFQ 정보를 불러올 수 없습니다."); + return; + } + + if (!vendorEmailInfos || vendorEmailInfos.length === 0) { + toast.error("선택된 벤더의 이메일 정보를 찾을 수 없습니다."); + return; + } + + // 다이얼로그 데이터 설정 + setSendDialogData({ + rfqInfo: rfqSendData.rfqInfo, + attachments: rfqSendData.attachments || [], + selectedVendors: vendorEmailInfos.map(v => ({ + vendorId: v.vendorId, + vendorName: v.vendorName, + vendorCode: v.vendorCode, + vendorCountry: v.vendorCountry, + vendorEmail: v.vendorEmail, + representativeEmail: v.representativeEmail, + contacts: v.contacts || [], + contactsByPosition: v.contactsByPosition || {}, + primaryEmail: v.primaryEmail, + currency: v.currency, + })), + }); + + // 다이얼로그 열기 + setIsSendDialogOpen(true); + } catch (error) { + console.error("RFQ 발송 데이터 로드 실패:", error); + toast.error("데이터를 불러오는데 실패했습니다. 다시 시도해주세요."); + } finally { + setIsLoadingSendData(false); + } + }, [selectedRows, rfqId]); // RFQ 발송 핸들러 const handleSendRfq = React.useCallback(async (data: { @@ -248,6 +319,12 @@ export function RfqVendorTable({ // 성공 후 처리 setSelectedRows([]); + setSendDialogData({ + rfqInfo: null, + attachments: [], + selectedVendors: [], + }); + toast.success(`${data.vendors.length}개 업체에 RFQ를 발송했습니다.`); } catch (error) { console.error("RFQ 발송 실패:", error); @@ -264,30 +341,63 @@ export function RfqVendorTable({ break; case "send": - // RFQ 발송 로직 - toast.info(`${vendor.vendorName}에게 RFQ를 발송합니다.`); + // 개별 RFQ 발송 + try { + setIsLoadingSendData(true); + + const [rfqSendData, vendorEmailInfos] = await Promise.all([ + getRfqSendData(rfqId), + getSelectedVendorsWithEmails(rfqId, [vendor.vendorId]) + ]); + + if (!rfqSendData?.rfqInfo || !vendorEmailInfos || vendorEmailInfos.length === 0) { + toast.error("벤더 정보를 불러올 수 없습니다."); + return; + } + + setSendDialogData({ + rfqInfo: rfqSendData.rfqInfo, + attachments: rfqSendData.attachments || [], + selectedVendors: vendorEmailInfos.map(v => ({ + vendorId: v.vendorId, + vendorName: v.vendorName, + vendorCode: v.vendorCode, + vendorCountry: v.vendorCountry, + vendorEmail: v.vendorEmail, + representativeEmail: v.representativeEmail, + contacts: v.contacts || [], + contactsByPosition: v.contactsByPosition || {}, + primaryEmail: v.primaryEmail, + currency: v.currency, + })), + }); + + setIsSendDialogOpen(true); + } catch (error) { + console.error("개별 발송 데이터 로드 실패:", error); + toast.error("데이터를 불러오는데 실패했습니다."); + } finally { + setIsLoadingSendData(false); + } break; case "edit": - // 수정 로직 toast.info("수정 기능은 준비중입니다."); break; case "delete": - // 삭제 로직 if (confirm(`${vendor.vendorName}을(를) 삭제하시겠습니까?`)) { toast.success(`${vendor.vendorName}이(가) 삭제되었습니다.`); } break; case "response-detail": - // 회신 상세 보기 toast.info(`${vendor.vendorName}의 회신 상세를 확인합니다.`); break; } - }, []); + }, [rfqId]); - // 컬럼 정의 (확장된 버전) + // 컬럼 정의 const columns: ColumnDef<any>[] = React.useMemo(() => [ { id: "select", @@ -535,7 +645,6 @@ export function RfqVendorTable({ header: ({ column }) => <ClientDataTableColumnHeaderSimple column={column} title="참여여부 (회신일)" />, cell: ({ row }) => { const submittedAt = row.original.response?.submittedAt; - const status = row.original.response?.status; if (!submittedAt) { return <Badge variant="outline">미참여</Badge>; @@ -639,7 +748,10 @@ export function RfqVendorTable({ 상세보기 </DropdownMenuItem> {!hasResponse && ( - <DropdownMenuItem onClick={() => handleAction("send", vendor)}> + <DropdownMenuItem + onClick={() => handleAction("send", vendor)} + disabled={isLoadingSendData} + > <Send className="mr-2 h-4 w-4" /> RFQ 발송 </DropdownMenuItem> @@ -662,7 +774,7 @@ export function RfqVendorTable({ }, size: 60, }, - ], [handleAction, rfqCode]); + ], [handleAction, rfqCode, isLoadingSendData]); const advancedFilterFields: DataTableAdvancedFilterField<any>[] = [ { id: "vendorName", label: "벤더명", type: "text" }, @@ -701,41 +813,6 @@ export function RfqVendorTable({ })); }, [selectedRows]); - // 선택된 벤더 정보 (Send용) - const selectedVendorsForSend = React.useMemo(() => { - return selectedRows.map(row => ({ - vendorId: row.vendorId, - vendorName: row.vendorName, - vendorCode: row.vendorCode, - vendorCountry: row.vendorCountry, - vendorEmail: row.vendorEmail || `vendor${row.vendorId}@example.com`, - currency: row.currency, - })); - }, [selectedRows]); - - // RFQ 정보 준비 (다이얼로그용) - const rfqInfoForDialog = React.useMemo(() => { - // props로 받은 rfqInfo 사용, 없으면 기본값 - return rfqInfo || { - rfqCode: rfqCode || '', - rfqTitle: '테스트 RFQ', - rfqType: '정기견적', - projectCode: 'PN003', - projectName: 'PETRONAS ZLNG nearshore project', - picName: '김*종', - picCode: '86D', - picTeam: '해양구매팀(해양구매1)', - packageNo: 'MM03', - packageName: 'Deck Machinery', - designPicName: '이*진', - designTeam: '전장설계팀 (전장기기시스템)', - materialGroup: 'BE2101', - materialGroupDesc: 'Combined Windlass & Mooring Wi', - dueDate: new Date('2025-07-05'), - evaluationApply: true, - }; - }, [rfqInfo, rfqCode]); - // 추가 액션 버튼들 const additionalActions = React.useMemo(() => ( <div className="flex items-center gap-2"> @@ -743,6 +820,7 @@ export function RfqVendorTable({ variant="outline" size="sm" onClick={() => setIsAddDialogOpen(true)} + disabled={isLoadingSendData} > <Plus className="h-4 w-4 mr-2" /> 벤더 추가 @@ -753,6 +831,7 @@ export function RfqVendorTable({ variant="outline" size="sm" onClick={() => setIsBatchUpdateOpen(true)} + disabled={isLoadingSendData} > <Settings2 className="h-4 w-4 mr-2" /> 정보 일괄 입력 ({selectedRows.length}) @@ -761,9 +840,19 @@ export function RfqVendorTable({ variant="outline" size="sm" onClick={handleBulkSend} + disabled={isLoadingSendData || selectedRows.length === 0} > - <Send className="h-4 w-4 mr-2" /> - 선택 발송 ({selectedRows.length}) + {isLoadingSendData ? ( + <> + <Loader2 className="mr-2 h-4 w-4 animate-spin" /> + 데이터 준비중... + </> + ) : ( + <> + <Send className="h-4 w-4 mr-2" /> + 선택 발송 ({selectedRows.length}) + </> + )} </Button> </> )} @@ -777,13 +866,13 @@ export function RfqVendorTable({ toast.success("데이터를 새로고침했습니다."); }, 1000); }} - disabled={isRefreshing} + disabled={isRefreshing || isLoadingSendData} > <RefreshCw className={cn("h-4 w-4 mr-2", isRefreshing && "animate-spin")} /> 새로고침 </Button> </div> - ), [selectedRows, isRefreshing, handleBulkSend]); + ), [selectedRows, isRefreshing, isLoadingSendData, handleBulkSend]); return ( <> @@ -828,9 +917,9 @@ export function RfqVendorTable({ <SendRfqDialog open={isSendDialogOpen} onOpenChange={setIsSendDialogOpen} - selectedVendors={selectedVendorsForSend} - rfqInfo={rfqInfoForDialog} - attachments={attachments || []} + selectedVendors={sendDialogData.selectedVendors} + rfqInfo={sendDialogData.rfqInfo} + attachments={sendDialogData.attachments || []} onSend={handleSendRfq} /> |
