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/pq/pq-review-table-new/vendors-table.tsx | 308 +++++++++++++++++++++++++++
 1 file changed, 308 insertions(+)
 create mode 100644 lib/pq/pq-review-table-new/vendors-table.tsx

(limited to 'lib/pq/pq-review-table-new/vendors-table.tsx')

diff --git a/lib/pq/pq-review-table-new/vendors-table.tsx b/lib/pq/pq-review-table-new/vendors-table.tsx
new file mode 100644
index 00000000..e1c4cefe
--- /dev/null
+++ b/lib/pq/pq-review-table-new/vendors-table.tsx
@@ -0,0 +1,308 @@
+"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 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 { getPQSubmissions } from "../service"
+import { getColumns, PQSubmission } from "./vendors-table-columns"
+import { VendorsTableToolbarActions } from "./vendors-table-toolbar-actions"
+import { PQFilterSheet } from "./pq-filter-sheet"
+import { cn } from "@/lib/utils"
+// TablePresetManager 관련 import 추가
+import { useTablePresets } from "@/components/data-table/use-table-presets"
+import { TablePresetManager } from "@/components/data-table/data-table-preset"
+import { useMemo } from "react"
+
+interface PQSubmissionsTableProps {
+  promises: Promise<[Awaited<ReturnType<typeof getPQSubmissions>>]>
+  className?: string
+}
+
+export function PQSubmissionsTable({ promises, className }: PQSubmissionsTableProps) {
+  const [rowAction, setRowAction] = React.useState<DataTableRowAction<PQSubmission> | null>(null)
+  const [isFilterPanelOpen, setIsFilterPanelOpen] = React.useState(false)
+  
+  const router = useRouter()
+  const searchParams = useSearchParams()
+
+  // Container wrapper의 위치를 측정하기 위한 ref
+  const containerRef = React.useRef<HTMLDivElement>(null)
+  const [containerTop, setContainerTop] = React.useState(0)
+
+  // Container 위치 측정 함수 - top만 측정
+  const updateContainerBounds = React.useCallback(() => {
+    if (containerRef.current) {
+      const rect = containerRef.current.getBoundingClientRect()
+      setContainerTop(rect.top)
+    }
+  }, [])
+
+  // 컴포넌트 마운트 시와 윈도우 리사이즈 시 위치 업데이트
+  React.useEffect(() => {
+    updateContainerBounds()
+    
+    const handleResize = () => {
+      updateContainerBounds()
+    }
+    
+    window.addEventListener('resize', handleResize)
+    window.addEventListener('scroll', updateContainerBounds)
+    
+    return () => {
+      window.removeEventListener('resize', handleResize)
+      window.removeEventListener('scroll', updateContainerBounds)
+    }
+  }, [updateContainerBounds])
+
+  // Suspense 방식으로 데이터 처리
+  const [promiseData] = React.use(promises)
+  const tableData = promiseData
+
+  // 디버깅용 로그
+  console.log("PQ Table Data:", {
+    dataLength: tableData.data?.length,
+    pageCount: tableData.pageCount,
+    sampleData: tableData.data?.[0]
+  })
+
+  // 초기 설정 정의 (RFQ와 동일한 패턴)
+  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') || searchParams.get('pqBasicFilters') ? 
+      JSON.parse(searchParams.get('basicFilters') || searchParams.get('pqBasicFilters')!) : [],
+    basicJoinOperator: (searchParams.get('basicJoinOperator') as "and" | "or") || "and",
+    search: searchParams.get('search') || '',
+    from: searchParams.get('from') || undefined,
+    to: searchParams.get('to') || undefined,
+    columnVisibility: {},
+    columnOrder: [],
+    pinnedColumns: { left: [], right: ["actions"] }, // PQ는 actions를 오른쪽에 고정
+    groupBy: [],
+    expandedRows: []
+  }), [searchParams])
+
+  // DB 기반 프리셋 훅 사용
+  const {
+    presets,
+    activePresetId,
+    hasUnsavedChanges,
+    isLoading: presetsLoading,
+    createPreset,
+    applyPreset,
+    updatePreset,
+    deletePreset,
+    setDefaultPreset,
+    renamePreset,
+    updateClientState,
+    getCurrentSettings,
+  } = useTablePresets<PQSubmission>('pq-submissions-table', initialSettings)
+
+  const columns = React.useMemo(
+    () => getColumns({ setRowAction, router }),
+    [setRowAction, router]
+  )
+
+  // PQ 제출 필터링을 위한 필드 정의
+  const filterFields: DataTableFilterField<PQSubmission>[] = [
+    { id: "vendorName", label: "협력업체" },
+    { id: "projectName", label: "프로젝트" },
+    { id: "status", label: "상태" },
+  ]
+
+  // 고급 필터 필드 정의
+  const advancedFilterFields: DataTableAdvancedFilterField<PQSubmission>[] = [
+    { id: "requesterName", label: "요청자명", type: "text" },
+    { id: "pqNumber", label: "PQ 번호", type: "text" },
+    { id: "vendorName", label: "협력업체명", type: "text" },
+    { id: "vendorCode", label: "협력업체 코드", type: "text" },
+    { id: "type", label: "PQ 유형", type: "select", options: [
+      { label: "일반 PQ", value: "GENERAL" },
+      { label: "프로젝트 PQ", value: "PROJECT" },
+    ]},
+    { id: "projectName", label: "프로젝트명", type: "text" },
+    { id: "status", label: "PQ 상태", type: "select", options: [
+      { label: "요청됨", value: "REQUESTED" },
+      { label: "진행 중", value: "IN_PROGRESS" },
+      { label: "제출됨", value: "SUBMITTED" },
+      { label: "승인됨", value: "APPROVED" },
+      { label: "거부됨", value: "REJECTED" },
+    ]},
+    { id: "evaluationResult", label: "평가 결과", type: "select", options: [
+      { label: "승인", value: "APPROVED" },
+      { label: "보완", value: "SUPPLEMENT" },
+      { label: "불가", value: "REJECTED" },
+    ]},
+    { id: "createdAt", label: "생성일", type: "date" },
+    { id: "submittedAt", label: "제출일", type: "date" },
+    { id: "approvedAt", label: "승인일", type: "date" },
+    { id: "rejectedAt", label: "거부일", type: "date" },
+  ]
+
+  // 현재 설정 가져오기
+  const currentSettings = useMemo(() => {
+    return getCurrentSettings()
+  }, [getCurrentSettings])
+  
+  // useDataTable 초기 상태 설정 (RFQ와 동일한 패턴)
+  const initialState = useMemo(() => {
+    return {
+      sorting: initialSettings.sort.filter(sortItem => {
+        const columnExists = columns.some(col => col.accessorKey === sortItem.id)
+        return columnExists
+      }) as any,
+      columnVisibility: currentSettings.columnVisibility,
+      columnPinning: currentSettings.pinnedColumns,
+    }
+  }, [currentSettings, initialSettings.sort, columns])
+
+  const { table } = useDataTable({
+    data: tableData.data,
+    columns,
+    pageCount: tableData.pageCount,
+    rowCount: tableData.total || tableData.data.length, // total 추가
+    filterFields, // RFQ와 달리 빈 배열이 아닌 실제 필터 필드 사용
+    enablePinning: true,
+    enableAdvancedFilter: true,
+    initialState,
+    getRowId: (originalRow) => String(originalRow.id),
+    shallow: false,
+    clearOnDefault: true,
+  })
+
+  // 조회 버튼 클릭 핸들러 - PQFilterSheet에 전달
+  const handleSearch = () => {
+    // Close the panel after search
+    setIsFilterPanelOpen(false)
+  }
+
+  // Get active basic filter count
+  const getActiveBasicFilterCount = () => {
+    try {
+      const basicFilters = searchParams.get('basicFilters') || searchParams.get('pqBasicFilters')
+      return basicFilters ? JSON.parse(basicFilters).length : 0
+    } catch (e) {
+      return 0
+    }
+  }
+
+  // Filter panel width
+  const FILTER_PANEL_WIDTH = 400;
+
+  return (
+    <>
+      {/* Filter Panel - fixed positioning으로 화면 최대 좌측에서 시작 */}
+      <div 
+        className={cn(
+          "fixed left-0 bg-background border-r z-50 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: `${containerTop}px`,
+          height: `calc(100vh - ${containerTop}px)`
+        }}
+      >
+        {/* Filter Content */}
+        <div className="h-full">
+          <PQFilterSheet 
+            isOpen={isFilterPanelOpen} 
+            onClose={() => setIsFilterPanelOpen(false)}
+            onSearch={handleSearch}
+            isLoading={false}
+          />
+        </div>
+      </div>
+
+      {/* Main Content Container */}
+      <div 
+        ref={containerRef}
+        className={cn("relative w-full overflow-hidden", className)}
+      >
+        <div className="flex w-full h-full">
+          {/* Main Content - 너비 조정으로 필터 패널 공간 확보 */}
+          <div 
+            className="flex flex-col min-w-0 overflow-hidden transition-all duration-300 ease-in-out"
+            style={{
+              width: isFilterPanelOpen ? `calc(100% - ${FILTER_PANEL_WIDTH}px)` : '100%',
+              marginLeft: isFilterPanelOpen ? `${FILTER_PANEL_WIDTH}px` : '0px'
+            }}
+          >
+            {/* Header Bar */}
+            <div className="flex items-center justify-between p-4 bg-background shrink-0">
+              <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 || tableData.data.length}건</span>
+                )}
+              </div>
+            </div>
+
+            {/* Table Content Area */}
+            <div className="flex-1 overflow-hidden" style={{ height: 'calc(100vh - 380px)' }}>
+              <div className="h-full w-full">
+                <DataTable table={table} className="h-full">
+                  <DataTableAdvancedToolbar
+                    table={table}
+                    filterFields={advancedFilterFields}
+                    shallow={false}
+                  >
+                    <div className="flex items-center gap-2">
+                      {/* DB 기반 테이블 프리셋 매니저 추가 */}
+                      <TablePresetManager<PQSubmission>
+                        presets={presets}
+                        activePresetId={activePresetId}
+                        currentSettings={currentSettings}
+                        hasUnsavedChanges={hasUnsavedChanges}
+                        isLoading={presetsLoading}
+                        onCreatePreset={createPreset}
+                        onUpdatePreset={updatePreset}
+                        onDeletePreset={deletePreset}
+                        onApplyPreset={applyPreset}
+                        onSetDefaultPreset={setDefaultPreset}
+                        onRenamePreset={renamePreset}
+                      />
+                      
+                      {/* 기존 툴바 액션들 */}
+                      <VendorsTableToolbarActions table={table} />
+                    </div>
+                  </DataTableAdvancedToolbar>
+                </DataTable>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </>
+  )
+}
\ No newline at end of file
-- 
cgit v1.2.3

