// lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx "use client" import * as React from "react" import { useSearchParams } from "next/navigation" import { type DataTableAdvancedFilterField, type DataTableFilterField } from "@/types/table" import { useDataTable } from "@/hooks/use-data-table" import { DataTable } from "@/components/data-table/data-table" import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar" import { TechSalesVendorQuotations, TECH_SALES_QUOTATION_STATUSES, TECH_SALES_QUOTATION_STATUS_CONFIG } from "@/db/schema" import { useRouter } from "next/navigation" import { getColumns } from "./vendor-quotations-table-columns" import { TechSalesRfqAttachmentsSheet, ExistingTechSalesAttachment } from "../../table/tech-sales-rfq-attachments-sheet" import { getTechSalesRfqAttachments, getVendorQuotations } from "@/lib/techsales-rfq/service" import { toast } from "sonner" import { Skeleton } from "@/components/ui/skeleton" interface QuotationWithRfqCode extends TechSalesVendorQuotations { rfqCode?: string | null; materialCode?: string | null; dueDate?: Date; rfqStatus?: string; itemName?: string | null; projNm?: string | null; quotationCode?: string | null; quotationVersion?: number | null; rejectionReason?: string | null; acceptedAt?: Date | null; attachmentCount?: number; } interface VendorQuotationsTableProps { vendorId: string; } // 로딩 스켈레톤 컴포넌트 function TableLoadingSkeleton() { return (
{/* 툴바 스켈레톤 */}
{/* 테이블 헤더 스켈레톤 */}
{/* 테이블 행 스켈레톤 */} {Array.from({ length: 5 }).map((_, index) => (
))}
{/* 페이지네이션 스켈레톤 */}
) } // 중앙 로딩 인디케이터 컴포넌트 function CenterLoadingIndicator() { return (

데이터를 불러오는 중...

잠시만 기다려주세요.

) } export function VendorQuotationsTable({ vendorId }: VendorQuotationsTableProps) { const searchParams = useSearchParams() const router = useRouter() // 첨부파일 시트 상태 const [attachmentsOpen, setAttachmentsOpen] = React.useState(false) const [selectedRfqForAttachments, setSelectedRfqForAttachments] = React.useState<{ id: number; rfqCode: string | null; status: string } | null>(null) const [attachmentsDefault, setAttachmentsDefault] = React.useState([]) // 데이터 로딩 상태 const [data, setData] = React.useState([]) const [pageCount, setPageCount] = React.useState(0) const [total, setTotal] = React.useState(0) const [isLoading, setIsLoading] = React.useState(true) const [isInitialLoad, setIsInitialLoad] = React.useState(true) // 최초 로딩 구분 // URL 파라미터에서 설정 읽기 const initialSettings = React.useMemo(() => ({ page: parseInt(searchParams?.get('page') || '1'), perPage: parseInt(searchParams?.get('perPage') || '10'), sort: searchParams?.get('sort') ? JSON.parse(searchParams.get('sort')!) : [{ id: "updatedAt", desc: true }], filters: searchParams?.get('filters') ? JSON.parse(searchParams.get('filters')!) : [], joinOperator: (searchParams?.get('joinOperator') as "and" | "or") || "and", basicFilters: searchParams?.get('basicFilters') ? JSON.parse(searchParams.get('basicFilters')!) : [], basicJoinOperator: (searchParams?.get('basicJoinOperator') as "and" | "or") || "and", search: searchParams?.get('search') || '', from: searchParams?.get('from') || '', to: searchParams?.get('to') || '', }), [searchParams]) // 데이터 로드 함수 const loadData = React.useCallback(async () => { try { setIsLoading(true) console.log('🔍 [VendorQuotationsTable] 데이터 로드 요청:', { vendorId, settings: initialSettings }) const result = await getVendorQuotations({ page: initialSettings.page, perPage: initialSettings.perPage, sort: initialSettings.sort, filters: initialSettings.filters, joinOperator: initialSettings.joinOperator, basicFilters: initialSettings.basicFilters, basicJoinOperator: initialSettings.basicJoinOperator, search: initialSettings.search, from: initialSettings.from, to: initialSettings.to, }, vendorId) console.log('🔍 [VendorQuotationsTable] 데이터 로드 결과:', { dataLength: result.data.length, pageCount: result.pageCount, total: result.total }) setData(result.data as QuotationWithRfqCode[]) setPageCount(result.pageCount) setTotal(result.total) } catch (error) { console.error('데이터 로드 오류:', error) toast.error('데이터를 불러오는 중 오류가 발생했습니다.') } finally { setIsLoading(false) setIsInitialLoad(false) } }, [vendorId, initialSettings]) // URL 파라미터 변경 감지 및 데이터 재로드 (초기 로드 포함) React.useEffect(() => { loadData() }, [ searchParams?.get('page'), searchParams?.get('perPage'), searchParams?.get('sort'), searchParams?.get('filters'), searchParams?.get('joinOperator'), searchParams?.get('basicFilters'), searchParams?.get('basicJoinOperator'), searchParams?.get('search'), searchParams?.get('from'), searchParams?.get('to'), // vendorId 변경도 감지 vendorId ]) // 데이터 안정성을 위한 메모이제이션 const stableData = React.useMemo(() => { return data; }, [data.length, data.map(item => `${item.id}-${item.status}-${item.updatedAt}`).join(',')]); // 첨부파일 시트 열기 함수 const openAttachmentsSheet = React.useCallback(async (rfqId: number) => { try { // RFQ 정보 조회 (data에서 rfqId에 해당하는 데이터 찾기) const quotationWithRfq = data.find(item => item.rfqId === rfqId) if (!quotationWithRfq) { toast.error("RFQ 정보를 찾을 수 없습니다.") return } // 실제 첨부파일 목록 조회 API 호출 const result = await getTechSalesRfqAttachments(rfqId) if (result.error) { toast.error(result.error) return } // API 응답을 ExistingTechSalesAttachment 형식으로 변환 const attachments: ExistingTechSalesAttachment[] = result.data.map(att => ({ id: att.id, techSalesRfqId: att.techSalesRfqId || rfqId, fileName: att.fileName, originalFileName: att.originalFileName, filePath: att.filePath, fileSize: att.fileSize || undefined, fileType: att.fileType || undefined, attachmentType: att.attachmentType as "RFQ_COMMON" | "VENDOR_SPECIFIC", description: att.description || undefined, createdBy: att.createdBy, createdAt: att.createdAt, })) setAttachmentsDefault(attachments) setSelectedRfqForAttachments({ id: rfqId, rfqCode: quotationWithRfq.rfqCode || null, status: quotationWithRfq.rfqStatus || "Unknown" }) setAttachmentsOpen(true) } catch (error) { console.error("첨부파일 조회 오류:", error) toast.error("첨부파일 조회 중 오류가 발생했습니다.") } }, [data]) // 테이블 컬럼 정의 const columns = React.useMemo(() => getColumns({ router, openAttachmentsSheet, }), [router, openAttachmentsSheet]) // 필터 필드 const filterFields = React.useMemo[]>(() => [ { id: "status", label: "상태", options: Object.entries(TECH_SALES_QUOTATION_STATUSES).map(([, statusValue]) => ({ label: TECH_SALES_QUOTATION_STATUS_CONFIG[statusValue].label, value: statusValue, })) }, { id: "rfqCode", label: "RFQ 번호", placeholder: "RFQ 번호 검색...", }, { id: "materialCode", label: "자재 코드", placeholder: "자재 코드 검색...", } ], []) // 고급 필터 필드 const advancedFilterFields = React.useMemo[]>(() => [ { id: "rfqCode", label: "RFQ 번호", type: "text", }, { id: "materialCode", label: "자재 코드", type: "text", }, { id: "status", label: "상태", type: "multi-select", options: Object.entries(TECH_SALES_QUOTATION_STATUSES).map(([, statusValue]) => ({ label: TECH_SALES_QUOTATION_STATUS_CONFIG[statusValue].label, value: statusValue, })), }, { id: "validUntil", label: "유효기간", type: "date", }, { id: "submittedAt", label: "제출일", type: "date", }, ], []) // useDataTable 훅 사용 const { table } = useDataTable({ data: stableData, columns, pageCount, rowCount: total, filterFields, enablePinning: true, enableAdvancedFilter: true, enableColumnResizing: true, columnResizeMode: 'onChange', initialState: { sorting: initialSettings.sort, columnPinning: { right: ["actions"] }, }, getRowId: (originalRow) => String(originalRow.id), shallow: false, clearOnDefault: true, defaultColumn: { minSize: 50, maxSize: 500, }, }) // 최초 로딩 시 전체 스켈레톤 표시 if (isInitialLoad && isLoading) { return (
) } return (
{/* 로딩 오버레이 (재로딩 시) */} {/* {!isInitialLoad && isLoading && (
)} */} {!isInitialLoad && isLoading && (
데이터 업데이트 중...
)}
{/* 첨부파일 관리 시트 (읽기 전용) */} {}} // 읽기 전용이므로 빈 함수 readOnly={true} // 벤더 쪽에서는 항상 읽기 전용 />
); }