"use client" import * as React from "react" import { useEffect, useState, useCallback, useMemo } from "react" import { DataTableRowAction, getRfqDetailColumns, RfqDetailView } from "./rfq-detail-column" import { toast } from "sonner" import { Skeleton } from "@/components/ui/skeleton" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Loader2, UserPlus, BarChart2, Send, Trash2 } from "lucide-react" import { ClientDataTable } from "@/components/client-data-table/data-table" import { AddVendorDialog } from "./add-vendor-dialog" import { DeleteVendorDialog } from "./delete-vendor-dialog" import { VendorCommunicationDrawer } from "./vendor-communication-drawer" import { VendorQuotationComparisonDialog } from "./vendor-quotation-comparison-dialog" // 기본적인 RFQ 타입 정의 interface TechSalesRfq { id: number rfqCode: string | null status: string materialCode?: string | null itemName?: string | null remark?: string | null rfqSendDate?: Date | null dueDate?: Date | null createdByName?: string | null // 필요에 따라 다른 필드들 추가 [key: string]: any // eslint-disable-line @typescript-eslint/no-explicit-any } // 프로퍼티 정의 interface RfqDetailTablesProps { selectedRfq: TechSalesRfq | null maxHeight?: string | number } export function RfqDetailTables({ selectedRfq, maxHeight }: RfqDetailTablesProps) { // console.log("selectedRfq", selectedRfq) // 상태 관리 const [isLoading, setIsLoading] = useState(false) const [details, setDetails] = useState([]) const [vendorDialogOpen, setVendorDialogOpen] = React.useState(false) const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false) const [selectedDetail, setSelectedDetail] = React.useState(null) const [isAdddialogLoading, setIsAdddialogLoading] = useState(false) const [rowAction, setRowAction] = React.useState | null>(null) // 벤더 커뮤니케이션 상태 관리 const [communicationDrawerOpen, setCommunicationDrawerOpen] = useState(false) const [selectedVendor, setSelectedVendor] = useState(null) // 읽지 않은 메시지 개수 const [unreadMessages, setUnreadMessages] = useState>({}) // 견적 비교 다이얼로그 상태 관리 const [comparisonDialogOpen, setComparisonDialogOpen] = useState(false) // 테이블 선택 상태 관리 const [selectedRows, setSelectedRows] = useState([]) const [isSendingRfq, setIsSendingRfq] = useState(false) const [isDeletingVendors, setIsDeletingVendors] = useState(false) // selectedRfq ID 메모이제이션 (객체 참조 변경 방지) const selectedRfqId = useMemo(() => selectedRfq?.id, [selectedRfq?.id]) // existingVendorIds 메모이제이션 const existingVendorIds = useMemo(() => { return details.map(detail => Number(detail.vendorId)).filter(Boolean); }, [details]); // 읽지 않은 메시지 로드 함수 메모이제이션 const loadUnreadMessages = useCallback(async () => { if (!selectedRfqId) return; try { // TODO: 기술영업용 읽지 않은 메시지 수 가져오기 함수 구현 필요 // const unreadData = await fetchUnreadMessages(selectedRfqId); // setUnreadMessages(unreadData); setUnreadMessages({}); } catch (error) { console.error("읽지 않은 메시지 로드 오류:", error); } }, [selectedRfqId]); // 데이터 새로고침 함수 메모이제이션 const handleRefreshData = useCallback(async () => { if (!selectedRfqId) return try { // 실제 벤더 견적 데이터 다시 로딩 const { getTechSalesVendorQuotationsWithJoin } = await import("@/lib/techsales-rfq/service") const result = await getTechSalesVendorQuotationsWithJoin({ rfqId: selectedRfqId, page: 1, perPage: 1000, }) // 데이터 변환 const transformedData = result.data?.map(item => ({ ...item, detailId: item.id, rfqId: selectedRfqId, rfqCode: selectedRfq?.rfqCode || null, vendorId: item.vendorId ? Number(item.vendorId) : undefined, })) || [] setDetails(transformedData) // 읽지 않은 메시지 개수 업데이트 await loadUnreadMessages(); toast.success("데이터를 성공적으로 새로고침했습니다") } catch (error) { console.error("데이터 새로고침 오류:", error) toast.error("데이터를 새로고침하는 중 오류가 발생했습니다") } }, [selectedRfqId, selectedRfq?.rfqCode, loadUnreadMessages]) // 벤더 추가 핸들러 메모이제이션 const handleAddVendor = useCallback(async () => { try { setIsAdddialogLoading(true) setVendorDialogOpen(true) } catch (error) { console.error("데이터 로드 오류:", error) toast.error("벤더 정보를 불러오는 중 오류가 발생했습니다") } finally { setIsAdddialogLoading(false) } }, []) // RFQ 발송 핸들러 메모이제이션 const handleSendRfq = useCallback(async () => { if (selectedRows.length === 0) { toast.warning("발송할 벤더를 선택해주세요."); return; } if (!selectedRfqId) { toast.error("선택된 RFQ가 없습니다."); return; } try { setIsSendingRfq(true); // 기술영업 RFQ 발송 서비스 함수 호출 const vendorIds = selectedRows.map(row => row.vendorId).filter(Boolean); const { sendTechSalesRfqToVendors } = await import("@/lib/techsales-rfq/service"); const result = await sendTechSalesRfqToVendors({ rfqId: selectedRfqId, vendorIds: vendorIds as number[] }); if (result.success) { toast.success(result.message || `${selectedRows.length}개 벤더에게 RFQ가 발송되었습니다.`); } else { toast.error(result.message || "RFQ 발송 중 오류가 발생했습니다."); } // 선택 해제 setSelectedRows([]); // 데이터 새로고침 await handleRefreshData(); } catch (error) { console.error("RFQ 발송 오류:", error); toast.error("RFQ 발송 중 오류가 발생했습니다."); } finally { setIsSendingRfq(false); } }, [selectedRows, selectedRfqId, handleRefreshData]); // 벤더 삭제 핸들러 메모이제이션 const handleDeleteVendors = useCallback(async () => { if (selectedRows.length === 0) { toast.warning("삭제할 벤더를 선택해주세요."); return; } if (!selectedRfqId) { toast.error("선택된 RFQ가 없습니다."); return; } try { setIsDeletingVendors(true); const vendorIds = selectedRows.map(row => row.vendorId).filter(Boolean) as number[]; if (vendorIds.length === 0) { toast.error("유효한 벤더 ID가 없습니다."); return; } // 서비스 함수 호출 const { removeVendorsFromTechSalesRfq } = await import("@/lib/techsales-rfq/service"); const result = await removeVendorsFromTechSalesRfq({ rfqId: selectedRfqId, vendorIds: vendorIds }); if (result.error) { toast.error(result.error); } else { const successMessage = `${result.successCount}개의 벤더가 성공적으로 삭제되었습니다`; const errorMessage = result.errorCount && result.errorCount > 0 ? ` (${result.errorCount}개 실패)` : ""; toast.success(successMessage + errorMessage); } // 선택 해제 setSelectedRows([]); // 데이터 새로고침 await handleRefreshData(); } catch (error) { console.error("벤더 삭제 오류:", error); toast.error("벤더 삭제 중 오류가 발생했습니다."); } finally { setIsDeletingVendors(false); } }, [selectedRows, selectedRfqId, handleRefreshData]); // 견적 비교 다이얼로그 열기 핸들러 메모이제이션 const handleOpenComparisonDialog = useCallback(() => { // 제출된 견적이 있는 벤더가 최소 1개 이상 있는지 확인 const hasSubmittedQuotations = details.some(detail => detail.status === "Submitted" // RfqDetailView의 실제 필드 사용 ); if (!hasSubmittedQuotations) { toast.warning("제출된 견적이 없습니다."); return; } setComparisonDialogOpen(true); }, [details]) // 칼럼 정의 - unreadMessages 상태 전달 (메모이제이션) const columns = useMemo(() => getRfqDetailColumns({ setRowAction, unreadMessages }), [unreadMessages]) // 필터 필드 정의 (메모이제이션) const advancedFilterFields = useMemo( () => [ { id: "vendorName", label: "벤더명", type: "text", }, { id: "vendorCode", label: "벤더 코드", type: "text", }, { id: "currency", label: "통화", type: "text", }, ], [] ) // 계산된 값들 메모이제이션 const totalUnreadMessages = useMemo(() => Object.values(unreadMessages).reduce((sum, count) => sum + count, 0), [unreadMessages] ); const vendorsWithQuotations = useMemo(() => details.filter(detail => detail.status === "Submitted").length, [details] ); // RFQ ID가 변경될 때 데이터 로드 useEffect(() => { async function loadRfqDetails() { if (!selectedRfqId) { setDetails([]) return } try { setIsLoading(true) // 실제 벤더 견적 데이터 로딩 const { getTechSalesVendorQuotationsWithJoin } = await import("@/lib/techsales-rfq/service") const result = await getTechSalesVendorQuotationsWithJoin({ rfqId: selectedRfqId, page: 1, perPage: 1000, // 모든 데이터 가져오기 }) // 데이터 변환 (procurement 패턴에 맞게) const transformedData = result.data?.map(item => ({ ...item, detailId: item.id, rfqId: selectedRfqId, rfqCode: selectedRfq?.rfqCode || null, vendorId: item.vendorId ? Number(item.vendorId) : undefined, // 기타 필요한 필드 변환 })) || [] setDetails(transformedData) // 읽지 않은 메시지 개수 로드 await loadUnreadMessages(); } catch (error) { console.error("RFQ 디테일 로드 오류:", error) setDetails([]) toast.error("RFQ 세부정보를 불러오는 중 오류가 발생했습니다") } finally { setIsLoading(false) } } loadRfqDetails() }, [selectedRfqId, selectedRfq?.rfqCode, loadUnreadMessages]) // 주기적으로 읽지 않은 메시지 갱신 (60초마다) - 메모이제이션된 함수 사용 useEffect(() => { if (!selectedRfqId) return; const intervalId = setInterval(() => { loadUnreadMessages(); }, 60000); // 60초마다 갱신 return () => clearInterval(intervalId); }, [selectedRfqId, loadUnreadMessages]); // rowAction 처리 - procurement 패턴 적용 (메모이제이션) useEffect(() => { if (!rowAction) return const handleRowAction = async () => { try { // 통신 액션인 경우 드로어 열기 if (rowAction.type === "communicate") { setSelectedVendor(rowAction.row.original); setCommunicationDrawerOpen(true); // 해당 벤더의 읽지 않은 메시지를 0으로 설정 (메시지를 읽은 것으로 간주) const vendorId = rowAction.row.original.vendorId; if (vendorId) { setUnreadMessages(prev => ({ ...prev, [vendorId]: 0 })); } // rowAction 초기화 setRowAction(null); return; } // 삭제 액션인 경우 if (rowAction.type === "delete") { setSelectedDetail(rowAction.row.original); setDeleteDialogOpen(true); setRowAction(null); return; } } catch (error) { console.error("액션 처리 오류:", error); toast.error("작업을 처리하는 중 오류가 발생했습니다"); } }; handleRowAction(); }, [rowAction]) // 선택된 행 변경 핸들러 메모이제이션 const handleSelectedRowsChange = useCallback((selectedRowsData: RfqDetailView[]) => { setSelectedRows(selectedRowsData); }, []); // 커뮤니케이션 드로어 변경 핸들러 메모이제이션 const handleCommunicationDrawerChange = useCallback((open: boolean) => { setCommunicationDrawerOpen(open); // 드로어가 닫힐 때 읽지 않은 메시지 개수 갱신 if (!open) loadUnreadMessages(); }, [loadUnreadMessages]); if (!selectedRfq) { return (
RFQ를 선택하세요
) } // 로딩 중인 경우 if (isLoading) { return (
) } return (
{/* 테이블 또는 빈 상태 표시 */} {details.length > 0 ? (
{selectedRows.length > 0 && ( {selectedRows.length}개 선택됨 )} {totalUnreadMessages > 0 && ( 읽지 않은 메시지: {totalUnreadMessages}건 )} {vendorsWithQuotations > 0 && ( 견적 제출: {vendorsWithQuotations}개 벤더 )}
{/* RFQ 발송 버튼 */} {/* 벤더 삭제 버튼 */} {/* 견적 비교 버튼 */} {/* 벤더 추가 버튼 */}
) : (

벤더가 없습니다

벤더를 추가하여 RFQ를 시작하세요

)} {/* 다이얼로그들 */} {/* 벤더 커뮤니케이션 드로어 */} {/* 견적 비교 다이얼로그 */}
) }