From 20800b214145ee6056f94ca18fa1054f145eb977 Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Wed, 28 May 2025 00:32:31 +0000
Subject: (대표님) lib 파트 커밋
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 lib/procurement-rfqs/table/rfq-table.tsx | 411 +++++++++++++++++--------------
 1 file changed, 224 insertions(+), 187 deletions(-)

(limited to 'lib/procurement-rfqs/table/rfq-table.tsx')

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
-- 
cgit v1.2.3

