"use client"; import * as React from "react"; import { useRouter, useSearchParams } from "next/navigation"; import { Button } from "@/components/ui/button"; import { PanelLeftClose, PanelLeftOpen } from "lucide-react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Skeleton } from "@/components/ui/skeleton"; import type { DataTableAdvancedFilterField, DataTableFilterField, DataTableRowAction, } 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 { cn } from "@/lib/utils"; import { useTablePresets } from "@/components/data-table/use-table-presets"; import { TablePresetManager } from "@/components/data-table/data-table-preset"; import { RfqFilterSheet } from "./rfq-filter-sheet"; import { getRfqColumns } from "./rfq-table-columns"; import { RfqsLastView } from "@/db/schema"; import { getRfqs } from "../service"; import { RfqTableToolbarActions } from "./rfq-table-toolbar-actions"; import { RfqAttachmentsDialog } from "./rfq-attachments-dialog"; import { RfqItemsDialog } from "./rfq-items-dialog"; interface RfqTableProps { data: Awaited>; rfqCategory?: "general" | "itb" | "rfq"; className?: string; } export function RfqTable({ data, rfqCategory = "itb", className }: RfqTableProps) { const router = useRouter(); const searchParams = useSearchParams(); const [rowAction, setRowAction] = React.useState | null>(null); const [isFilterPanelOpen, setIsFilterPanelOpen] = React.useState(false); // 외부 필터 상태 const [externalFilters, setExternalFilters] = React.useState([]); const [externalJoinOperator, setExternalJoinOperator] = React.useState<"and" | "or">("and"); // 필터 적용 핸들러 const handleFiltersApply = React.useCallback((filters: any[], joinOperator: "and" | "or") => { console.log("=== 폼에서 필터 전달받음 ===", filters, joinOperator); setExternalFilters(filters); setExternalJoinOperator(joinOperator); setIsFilterPanelOpen(false); }, []); const searchString = React.useMemo( () => searchParams.toString(), [searchParams] ); const getSearchParam = React.useCallback( (key: string, def = "") => new URLSearchParams(searchString).get(key) ?? def, [searchString] ); // 초기 데이터 설정 const [tableData, setTableData] = React.useState(data); const [isDataLoading, setIsDataLoading] = React.useState(false); // URL 필터 변경 감지 및 데이터 새로고침 React.useEffect(() => { const refetchData = async () => { try { setIsDataLoading(true); const currentFilters = getSearchParam("filters"); const currentJoinOperator = getSearchParam("joinOperator", "and"); const currentPage = parseInt(getSearchParam("page", "1")); const currentPerPage = parseInt(getSearchParam("perPage", "10")); const currentSort = getSearchParam('sort') ? JSON.parse(getSearchParam('sort')!) : [{ id: "createdAt", desc: true }]; const currentSearch = getSearchParam("search", ""); const searchParams = { filters: currentFilters ? JSON.parse(currentFilters) : [], joinOperator: currentJoinOperator as "and" | "or", page: currentPage, perPage: currentPerPage, sort: currentSort, search: currentSearch, rfqCategory: rfqCategory, }; console.log("=== 새 데이터 요청 ===", searchParams); const newData = await getRfqs(searchParams); setTableData(newData); console.log("=== 데이터 업데이트 완료 ===", newData.data.length, "건"); } catch (error) { console.error("데이터 새로고침 오류:", error); } finally { setIsDataLoading(false); } }; const timeoutId = setTimeout(() => { const hasChanges = getSearchParam("filters") || getSearchParam("search") || getSearchParam("page") !== "1" || getSearchParam("perPage") !== "10" || getSearchParam("sort"); if (hasChanges) { refetchData(); } }, 300); return () => clearTimeout(timeoutId); }, [searchString, rfqCategory, getSearchParam]); const refreshData = React.useCallback(async () => { try { setIsDataLoading(true); const currentFilters = getSearchParam("filters"); const currentJoinOperator = getSearchParam("joinOperator", "and"); const currentPage = parseInt(getSearchParam("page", "1")); const currentPerPage = parseInt(getSearchParam("perPage", "10")); const currentSort = getSearchParam('sort') ? JSON.parse(getSearchParam('sort')!) : [{ id: "createdAt", desc: true }]; const currentSearch = getSearchParam("search", ""); const searchParams = { filters: currentFilters ? JSON.parse(currentFilters) : [], joinOperator: currentJoinOperator as "and" | "or", page: currentPage, perPage: currentPerPage, sort: currentSort, search: currentSearch, rfqCategory: rfqCategory, }; const newData = await getRfqs(searchParams); setTableData(newData); console.log("=== 데이터 새로고침 완료 ===", newData.data.length, "건"); } catch (error) { console.error("데이터 새로고침 오류:", error); } finally { setIsDataLoading(false); } }, [rfqCategory, getSearchParam]); // 컨테이너 위치 추적 const containerRef = React.useRef(null); const [containerTop, setContainerTop] = React.useState(0); const updateContainerBounds = React.useCallback(() => { if (containerRef.current) { const rect = containerRef.current.getBoundingClientRect(); const newTop = rect.top; setContainerTop(prevTop => { if (Math.abs(prevTop - newTop) > 1) { return newTop; } return prevTop; }); } }, []); React.useEffect(() => { updateContainerBounds(); const handleResize = () => { updateContainerBounds(); }; window.addEventListener('resize', handleResize); window.addEventListener('scroll', updateContainerBounds); return () => { window.removeEventListener('resize', handleResize); window.removeEventListener('scroll', updateContainerBounds); }; }, [updateContainerBounds]); const parseSearchParamHelper = React.useCallback((key: string, defaultValue: any): any => { try { const value = getSearchParam(key); return value ? JSON.parse(value) : defaultValue; } catch { return defaultValue; } }, [getSearchParam]); const parseSearchParam = (key: string, defaultValue: T): T => { return parseSearchParamHelper(key, defaultValue); }; // 테이블 설정 const initialSettings = React.useMemo(() => ({ page: parseInt(getSearchParam("page", "1")), perPage: parseInt(getSearchParam("perPage", "10")), sort: getSearchParam('sort') ? JSON.parse(getSearchParam('sort')!) : [{ id: "createdAt", desc: true }], filters: parseSearchParam("filters", []), joinOperator: (getSearchParam("joinOperator") as "and" | "or") || "and", search: getSearchParam("search", ""), columnVisibility: {}, columnOrder: [], pinnedColumns: { left: [], right: ["actions"] }, groupBy: [], expandedRows: [] }), [getSearchParam, parseSearchParam]); // 탭별로 독립적인 tableId 사용 (정렬 상태 분리) const tableId = React.useMemo(() => `rfq-table-${rfqCategory}`, [rfqCategory]); const { presets, activePresetId, hasUnsavedChanges, isLoading: presetsLoading, createPreset, applyPreset, updatePreset, deletePreset, setDefaultPreset, renamePreset, getCurrentSettings, } = useTablePresets(tableId, initialSettings); // 컬럼 정의 const columns = React.useMemo(() => { return getRfqColumns({ setRowAction, rfqCategory , router }); }, [rfqCategory, setRowAction, router]); const filterFields: DataTableFilterField[] = [ { id: "rfqCode", label: "견적 No." }, { id: "projectName", label: "프로젝트명" }, { id: "itemName", label: "자재명" }, { id: "status", label: "상태" }, ]; const advancedFilterFields: DataTableAdvancedFilterField[] = [ { id: "rfqCode", label: "견적 No.", type: "text" }, { id: "status", label: "견적상태", type: "select", options: [ { label: "RFQ 생성", value: "RFQ 생성" }, { label: "구매담당지정", value: "구매담당지정" }, { label: "견적요청문서 확정", value: "견적요청문서 확정" }, { label: "Short List 확정", value: "Short List 확정" }, { label: "TBE 완료", value: "TBE 완료" }, { label: "RFQ 발송", value: "RFQ 발송" }, { label: "견적접수", value: "견적접수" }, { label: "최종업체선정", value: "최종업체선정" }, ] }, { id: "projectCode", label: "프로젝트 코드", type: "text" }, { id: "projectName", label: "프로젝트명", type: "text" }, { id: "itemCode", label: "자재코드", type: "text" }, { id: "itemName", label: "자재명", type: "text" }, { id: "packageNo", label: "패키지 번호", type: "text" }, { id: "picUserName", label: "구매담당자", type: "text" }, { id: "vendorCount", label: "업체수", type: "number" }, { id: "dueDate", label: "마감일", type: "date" }, { id: "rfqSendDate", label: "발송일", type: "date" }, ...(rfqCategory === "general" ? [ { id: "rfqType", label: "견적 유형", type: "select", options: [ { label: "단가계약", value: "단가계약" }, { label: "매각계약", value: "매각계약" }, { label: "일반계약", value: "일반계약" }, ] }, { id: "rfqTitle", label: "견적 제목", type: "text" }, ] as DataTableAdvancedFilterField[] : []), ...(rfqCategory === "itb" ? [ { id: "smCode", label: "SM 코드", type: "text" }, ] as DataTableAdvancedFilterField[] : []), ...(rfqCategory === "rfq" ? [ { id: "prNumber", label: "PR 번호", type: "text" }, { id: "prIssueDate", label: "PR 발행일", type: "date" }, { id: "series", label: "시리즈", type: "select", options: [ { label: "시리즈 통합", value: "SS" }, { label: "품목 통합", value: "II" }, { label: "통합 없음", value: "" }, ] }, ] as DataTableAdvancedFilterField[] : []), ]; const currentSettings = React.useMemo(() => getCurrentSettings(), [getCurrentSettings]); // 탭별로 독립적인 정렬 상태 관리 // rfqCategory가 변경되면 정렬 상태를 재계산하여 탭 간 정렬 충돌 방지 const initialState = React.useMemo(() => { // 현재 탭의 컬럼에 존재하는 정렬만 유효한 것으로 필터링 const validSorting = initialSettings.sort.filter((s: any) => columns.some((c: any) => ("accessorKey" in c ? c.accessorKey : c.id) === s.id) ); // 유효한 정렬이 없으면 기본 정렬 사용 const sorting = validSorting.length > 0 ? validSorting : [{ id: "createdAt", desc: true }]; return { sorting, columnVisibility: currentSettings.columnVisibility, columnPinning: currentSettings.pinnedColumns, }; }, [columns, currentSettings, initialSettings.sort, rfqCategory]); const { table } = useDataTable({ data: tableData.data, columns, pageCount: tableData.pageCount, rowCount: tableData.total || tableData.data.length, filterFields, enablePinning: true, enableAdvancedFilter: true, initialState, getRowId: (originalRow) => String(originalRow.id), shallow: false, clearOnDefault: true, }); const getActiveFilterCount = React.useCallback(() => { try { const filtersParam = getSearchParam("filters"); if (filtersParam) { const filters = JSON.parse(filtersParam); return Array.isArray(filters) ? filters.length : 0; } return 0; } catch { return 0; } }, [getSearchParam]); const FILTER_PANEL_WIDTH = 400; return ( <> {/* Filter Panel */}
setIsFilterPanelOpen(false)} onFiltersApply={handleFiltersApply} rfqCategory={rfqCategory} isLoading={false} />
{/* Main Content Container */}
{/* Header Bar */}
{rfqCategory === "general" ? "일반견적" : rfqCategory === "itb" ? "ITB" : "RFQ"}
{tableData && ( 총 {tableData.total || tableData.data.length}건 )}
{/* Table Content Area */}
{isDataLoading && (
필터링 중...
)}
{ console.log("=== 필터 변경 감지 ===", filters, joinOperator); }} >
presets={presets} activePresetId={activePresetId} currentSettings={currentSettings} hasUnsavedChanges={hasUnsavedChanges} isLoading={presetsLoading} onCreatePreset={createPreset} onUpdatePreset={updatePreset} onDeletePreset={deletePreset} onApplyPreset={applyPreset} onSetDefaultPreset={setDefaultPreset} onRenamePreset={renamePreset} />
{/* 다이얼로그들 */} {rowAction?.type === "attachment" && ( setRowAction(null)} rfqData={rowAction.row.original} /> )} {rowAction?.type === "items" && ( setRowAction(null)} rfqData={rowAction.row.original} /> )} ); }