diff options
Diffstat (limited to 'lib/procurement-rfqs/table/rfq-table.tsx')
| -rw-r--r-- | lib/procurement-rfqs/table/rfq-table.tsx | 411 |
1 files changed, 224 insertions, 187 deletions
diff --git a/lib/procurement-rfqs/table/rfq-table.tsx b/lib/procurement-rfqs/table/rfq-table.tsx index 23cd66fa..ca976172 100644 --- a/lib/procurement-rfqs/table/rfq-table.tsx +++ b/lib/procurement-rfqs/table/rfq-table.tsx @@ -1,44 +1,86 @@ "use client" import * as React from "react" +import { useSearchParams } from "next/navigation" +import { Button } from "@/components/ui/button" +import { PanelLeftClose, PanelLeftOpen } from "lucide-react" import type { DataTableAdvancedFilterField, DataTableRowAction, } from "@/types/table" +import { + ResizablePanelGroup, + ResizablePanel, + ResizableHandle, +} from "@/components/ui/resizable" + import { useDataTable } from "@/hooks/use-data-table" import { DataTable } from "@/components/data-table/data-table" import { getColumns, EditingCellState } from "./rfq-table-column" -import { useEffect, useCallback, useRef, useMemo } from "react" +import { useEffect, useCallback, useRef, useMemo, useLayoutEffect } from "react" import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar" import { RFQTableToolbarActions } from "./rfq-table-toolbar-actions" import { ProcurementRfqsView } from "@/db/schema" import { getPORfqs } from "../services" import { toast } from "sonner" import { updateRfqRemark } from "@/lib/procurement-rfqs/services" -import { useSearchParams } from "next/navigation" import { useTablePresets } from "@/components/data-table/use-table-presets" import { TablePresetManager } from "@/components/data-table/data-table-preset" import { Loader2 } from "lucide-react" +import { RFQFilterSheet } from "./rfq-filter-sheet" +import { RfqDetailTables } from "./detail-table/rfq-detail-table" +import { cn } from "@/lib/utils" interface RFQListTableProps { - data?: Awaited<ReturnType<typeof getPORfqs>>; - onSelectRFQ?: (rfq: ProcurementRfqsView | null) => void; - onDataRefresh?: () => void; - maxHeight?: string | number; + promises: Promise<[Awaited<ReturnType<typeof getPORfqs>>]> + className?: string; + calculatedHeight?: string; // 계산된 높이 추가 } export function RFQListTable({ - data, - onSelectRFQ, - onDataRefresh, - maxHeight + promises, + className, + calculatedHeight }: RFQListTableProps) { const searchParams = useSearchParams() + + // 필터 패널 상태 + const [isFilterPanelOpen, setIsFilterPanelOpen] = React.useState(false) + + // 선택된 RFQ 상태 + const [selectedRfq, setSelectedRfq] = React.useState<ProcurementRfqsView | null>(null) + + // 패널 collapse 상태 + const [isTopCollapsed, setIsTopCollapsed] = React.useState(false) + const [panelHeight, setPanelHeight] = React.useState<number>(55) + + // refs + const headerRef = React.useRef<HTMLDivElement>(null) + + // 고정 높이 설정을 위한 상수 (실제 측정값으로 조정 필요) + const LAYOUT_HEADER_HEIGHT = 64 // Layout Header 높이 + const LAYOUT_FOOTER_HEIGHT = 60 // Layout Footer 높이 (있다면 실제 값) + const LOCAL_HEADER_HEIGHT = 72 // 로컬 헤더 바 높이 (p-4 + border) + const FILTER_PANEL_WIDTH = 400 // 필터 패널 너비 + + // 높이 계산 + // 필터 패널 높이 - Layout Header와 Footer 사이 + const FIXED_FILTER_HEIGHT = `calc(100vh - ${LAYOUT_HEADER_HEIGHT*2}px)` + + console.log(calculatedHeight) + + // 테이블 컨텐츠 높이 - 전달받은 높이에서 로컬 헤더 제외 + const FIXED_TABLE_HEIGHT = calculatedHeight + ? `calc(${calculatedHeight} - ${LOCAL_HEADER_HEIGHT}px)` + : `calc(100vh - ${LAYOUT_HEADER_HEIGHT + LAYOUT_FOOTER_HEIGHT + LOCAL_HEADER_HEIGHT+76}px)` // fallback + + // Suspense 방식으로 데이터 처리 + const [promiseData] = React.use(promises) + const tableData = promiseData + const [rowAction, setRowAction] = React.useState<DataTableRowAction<ProcurementRfqsView> | null>(null) const [editingCell, setEditingCell] = React.useState<EditingCellState | null>(null) - const [localData, setLocalData] = React.useState<typeof data>(data || { data: [], pageCount: 0, total: 0 }) - const [isMounted, setIsMounted] = React.useState(false) - + // 초기 설정 정의 const initialSettings = React.useMemo(() => ({ page: parseInt(searchParams.get('page') || '1'), @@ -70,39 +112,16 @@ export function RFQListTable({ deletePreset, setDefaultPreset, renamePreset, - updateClientState, getCurrentSettings, } = useTablePresets<ProcurementRfqsView>('rfq-list-table', initialSettings) - // 클라이언트 마운트 체크 - useEffect(() => { - setIsMounted(true) - }, []) - - // 데이터 변경 감지 - useEffect(() => { - setLocalData(data || { data: [], pageCount: 0, total: 0 }) - }, [data]) - // 비고 업데이트 함수 const updateRemark = async (rfqId: number, remark: string) => { try { - if (localData && localData.data) { - const rowIndex = localData.data.findIndex(row => row.id === rfqId); - if (rowIndex >= 0) { - const newData = [...localData.data]; - newData[rowIndex] = { ...newData[rowIndex], remark }; - setLocalData({ ...localData, data: newData }); - } - } - const result = await updateRfqRemark(rfqId, remark); if (result.success) { toast.success("비고가 업데이트되었습니다"); - if (onDataRefresh) { - onDataRefresh(); - } } else { toast.error(result.message || "업데이트 중 오류가 발생했습니다"); } @@ -117,9 +136,7 @@ export function RFQListTable({ if (rowAction) { switch (rowAction.type) { case "select": - if (onSelectRFQ) { - onSelectRFQ(rowAction.row.original) - } + setSelectedRfq(rowAction.row.original) break; case "update": console.log("Update rfq:", rowAction.row.original) @@ -130,7 +147,7 @@ export function RFQListTable({ } setRowAction(null) } - }, [rowAction, onSelectRFQ]) + }, [rowAction]) const columns = React.useMemo( () => getColumns({ @@ -198,7 +215,6 @@ export function RFQListTable({ // useDataTable 초기 상태 설정 const initialState = useMemo(() => { - console.log('Setting initial state:', currentSettings) return { sorting: initialSettings.sort.filter(sortItem => { const columnExists = columns.some(col => col.accessorKey === sortItem.id) @@ -209,167 +225,188 @@ export function RFQListTable({ } }, [currentSettings, initialSettings.sort, columns]) - // useDataTable 훅 설정 + // useDataTable 훅 설정 (PQ와 동일한 설정) const { table } = useDataTable({ - data: localData?.data || [], + data: tableData?.data || [], columns, - pageCount: localData?.pageCount || 0, - rowCount: localData?.total || 0, - filterFields: [], + pageCount: tableData?.pageCount || 0, + rowCount: tableData?.total || 0, + filterFields: [], // PQ와 동일하게 빈 배열 enablePinning: true, enableAdvancedFilter: true, initialState, getRowId: (originalRow) => String(originalRow.id), - shallow: false, + shallow: false, // PQ와 동일하게 false clearOnDefault: true, - columnResizeMode: "onEnd", }) - - // 테이블 상태 변경 감지 및 자동 저장 - const lastKnownStateRef = useRef<{ - columnVisibility: string - columnPinning: string - columnOrder: string[] - }>({ - columnVisibility: '{}', - columnPinning: '{"left":[],"right":[]}', - columnOrder: [] - }) - - const checkAndUpdateTableState = useCallback(() => { - if (!presetsLoading && !activePresetId) return - + + // 조회 버튼 클릭 핸들러 + const handleSearch = () => { + setIsFilterPanelOpen(false) + } + + // Get active basic filter count (PQ와 동일한 방식) + const getActiveBasicFilterCount = () => { try { - const currentVisibility = table.getState().columnVisibility - const currentPinning = table.getState().columnPinning - - // 컬럼 순서 가져오기 - const allColumns = table.getAllColumns() - const leftPinned = table.getLeftHeaderGroups()[0]?.headers.map(h => h.column.id) || [] - const rightPinned = table.getRightHeaderGroups()[0]?.headers.map(h => h.column.id) || [] - const center = table.getCenterHeaderGroups()[0]?.headers.map(h => h.column.id) || [] - const currentOrder = [...leftPinned, ...center, ...rightPinned] - - const visibilityString = JSON.stringify(currentVisibility) - const pinningString = JSON.stringify(currentPinning) - const orderString = JSON.stringify(currentOrder) - - // 실제 변경이 있을 때만 업데이트 - if ( - visibilityString !== lastKnownStateRef.current.columnVisibility || - pinningString !== lastKnownStateRef.current.columnPinning || - orderString !== JSON.stringify(lastKnownStateRef.current.columnOrder) - ) { - console.log('Table state changed, updating preset...') - - const newClientState = { - columnVisibility: currentVisibility, - columnOrder: currentOrder, - pinnedColumns: currentPinning, - } - - // 상태 업데이트 전에 기록 - lastKnownStateRef.current = { - columnVisibility: visibilityString, - columnPinning: pinningString, - columnOrder: currentOrder - } - - updateClientState(newClientState) - } - } catch (error) { - console.error('Error checking table state:', error) - } - }, [activePresetId, table, updateClientState, presetsLoading ]) - - // 주기적으로 테이블 상태 체크 - useEffect(() => { - if (!isMounted || !activePresetId) return - - console.log('Starting table state polling') - const intervalId = setInterval(checkAndUpdateTableState, 500) - - return () => { - clearInterval(intervalId) - console.log('Stopped table state polling') + const basicFilters = searchParams.get('basicFilters') + return basicFilters ? JSON.parse(basicFilters).length : 0 + } catch (e) { + return 0 } - }, [isMounted, activePresetId, checkAndUpdateTableState]) - - // 프리셋 적용 시 테이블 상태 업데이트 - useEffect(() => { - if (isMounted && activePresetId && currentSettings) { - const settings = currentSettings - console.log('Applying preset settings to table:', settings) - - const currentVisibility = table.getState().columnVisibility - const currentPinning = table.getState().columnPinning - - if ( - JSON.stringify(currentVisibility) !== JSON.stringify(settings.columnVisibility) || - JSON.stringify(currentPinning) !== JSON.stringify(settings.pinnedColumns) - ) { - console.log('Updating table state to match preset...') - - // 테이블 상태 업데이트 - table.setColumnVisibility(settings.columnVisibility) - table.setColumnPinning(settings.pinnedColumns) - - // 상태 저장소 업데이트 - lastKnownStateRef.current = { - columnVisibility: JSON.stringify(settings.columnVisibility), - columnPinning: JSON.stringify(settings.pinnedColumns), - columnOrder: settings.columnOrder || [] - } - } - } - }, [isMounted, activePresetId, currentSettings, table]) + } - // 로딩 중일 때는 스켈레톤 표시 - if (!isMounted) { - return ( - <div className="w-full h-96 flex items-center justify-center"> - <div className="flex flex-col items-center gap-2"> - <Loader2 className="h-8 w-8 animate-spin text-muted-foreground" /> - <span className="text-sm text-muted-foreground">테이블 설정을 로드하는 중...</span> + console.log(panelHeight) + + return ( + <div + className={cn("flex flex-col relative", className)} + style={{ height: calculatedHeight }} + > + {/* Filter Panel - 계산된 높이 적용 */} + <div + className={cn( + "fixed left-0 bg-background border-r z-30 flex flex-col transition-all duration-300 ease-in-out overflow-hidden", + isFilterPanelOpen ? "border-r shadow-lg" : "border-r-0" + )} + style={{ + width: isFilterPanelOpen ? `${FILTER_PANEL_WIDTH}px` : '0px', + top: `${LAYOUT_HEADER_HEIGHT*2}px`, + height: FIXED_FILTER_HEIGHT + }} + > + {/* Filter Content */} + <div className="h-full"> + <RFQFilterSheet + isOpen={isFilterPanelOpen} + onClose={() => setIsFilterPanelOpen(false)} + onSearch={handleSearch} + isLoading={false} + /> </div> </div> - ) - } - - return ( - <div className="w-full overflow-auto"> - <DataTable table={table} maxHeight={maxHeight}> - <DataTableAdvancedToolbar - table={table} - filterFields={advancedFilterFields} - shallow={false} + + {/* Main Content */} + <div + className="flex flex-col transition-all duration-300 ease-in-out" + style={{ + width: isFilterPanelOpen ? `calc(100% - ${FILTER_PANEL_WIDTH}px)` : '100%', + marginLeft: isFilterPanelOpen ? `${FILTER_PANEL_WIDTH}px` : '0px', + height: '100%' + }} + > + {/* Header Bar - 고정 높이 */} + <div + ref={headerRef} + className="flex items-center justify-between p-4 bg-background border-b" + style={{ + height: `${LOCAL_HEADER_HEIGHT}px`, + flexShrink: 0 + }} > - <div className="flex items-center gap-2"> - {/* DB 기반 테이블 프리셋 매니저 */} - <TablePresetManager<ProcurementRfqsView> - presets={presets} - activePresetId={activePresetId} - currentSettings={currentSettings} - hasUnsavedChanges={hasUnsavedChanges} - isLoading={presetsLoading} - onCreatePreset={createPreset} - onUpdatePreset={updatePreset} - onDeletePreset={deletePreset} - onApplyPreset={applyPreset} - onSetDefaultPreset={setDefaultPreset} - onRenamePreset={renamePreset} - /> - - {/* 기존 툴바 액션들 */} - <RFQTableToolbarActions - table={table} - localData={localData} - setLocalData={setLocalData} - onSuccess={onDataRefresh} - /> + <div className="flex items-center gap-3"> + <Button + variant="outline" + size="sm" + type='button' + onClick={() => setIsFilterPanelOpen(!isFilterPanelOpen)} + className="flex items-center shadow-sm" + > + {isFilterPanelOpen ? <PanelLeftClose className="size-4"/> : <PanelLeftOpen className="size-4"/>} + {getActiveBasicFilterCount() > 0 && ( + <span className="ml-2 bg-primary text-primary-foreground rounded-full px-2 py-0.5 text-xs"> + {getActiveBasicFilterCount()} + </span> + )} + </Button> + </div> + + {/* Right side info */} + <div className="text-sm text-muted-foreground"> + {tableData && ( + <span>총 {tableData.total || 0}건</span> + )} </div> - </DataTableAdvancedToolbar> - </DataTable> + </div> + + {/* Table Content Area - 계산된 높이 사용 */} + <div + className="relative bg-background" + style={{ + height: FIXED_TABLE_HEIGHT, + display: 'grid', + gridTemplateRows: '1fr', + gridTemplateColumns: '1fr' + }} + > + <ResizablePanelGroup + direction="vertical" + className="w-full h-full" + > + <ResizablePanel + defaultSize={60} + minSize={25} + maxSize={75} + collapsible={false} + onResize={(size) => { + setPanelHeight(size) + }} + className="flex flex-col overflow-hidden" + > + {/* 상단 테이블 영역 */} + <div className="flex-1 min-h-0 overflow-hidden"> + <DataTable + table={table} + // className="h-full" + maxHeight={`${panelHeight*0.5}vh`} + > + <DataTableAdvancedToolbar + table={table} + filterFields={advancedFilterFields} + shallow={false} + > + <div className="flex items-center gap-2"> + <TablePresetManager<ProcurementRfqsView> + presets={presets} + activePresetId={activePresetId} + currentSettings={currentSettings} + hasUnsavedChanges={hasUnsavedChanges} + isLoading={presetsLoading} + onCreatePreset={createPreset} + onUpdatePreset={updatePreset} + onDeletePreset={deletePreset} + onApplyPreset={applyPreset} + onSetDefaultPreset={setDefaultPreset} + onRenamePreset={renamePreset} + /> + + <RFQTableToolbarActions + table={table} + localData={tableData} + setLocalData={() => {}} + onSuccess={() => {}} + /> + </div> + </DataTableAdvancedToolbar> + </DataTable> + </div> + </ResizablePanel> + + <ResizableHandle withHandle /> + + <ResizablePanel + minSize={25} + defaultSize={40} + collapsible={false} + className="flex flex-col overflow-hidden" + > + {/* 하단 상세 테이블 영역 */} + <div className="flex-1 min-h-0 overflow-hidden bg-background"> + <RfqDetailTables selectedRfq={selectedRfq} maxHeight={`${(100-panelHeight)*0.4}vh`}/> + </div> + </ResizablePanel> + </ResizablePanelGroup> + </div> + </div> </div> ) }
\ No newline at end of file |
