From aa86729f9a2ab95346a2851e3837de1c367aae17 Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Fri, 20 Jun 2025 11:37:31 +0000
Subject: (대표님) 20250620 작업사항
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../table/evaluation-target-table.tsx              | 154 +++++++++++++--------
 1 file changed, 95 insertions(+), 59 deletions(-)

(limited to 'lib/evaluation-target-list/table/evaluation-target-table.tsx')

diff --git a/lib/evaluation-target-list/table/evaluation-target-table.tsx b/lib/evaluation-target-list/table/evaluation-target-table.tsx
index 15837733..fe0b3188 100644
--- a/lib/evaluation-target-list/table/evaluation-target-table.tsx
+++ b/lib/evaluation-target-list/table/evaluation-target-table.tsx
@@ -25,6 +25,7 @@ import { getEvaluationTargetsColumns } from "./evaluation-targets-columns"
 import { EvaluationTargetsTableToolbarActions } from "./evaluation-targets-toolbar-actions"
 import { EvaluationTargetFilterSheet } from "./evaluation-targets-filter-sheet"
 import { EvaluationTargetWithDepartments } from "@/db/schema"
+import { EditEvaluationTargetSheet } from "./update-evaluation-target"
 
 interface EvaluationTargetsTableProps {
   promises: Promise<[Awaited<ReturnType<typeof getEvaluationTargets>>]>
@@ -40,13 +41,13 @@ function EvaluationTargetsStats({ evaluationYear }: { evaluationYear: number })
 
   React.useEffect(() => {
     let isMounted = true
-    
+
     async function fetchStats() {
       try {
         setIsLoading(true)
         setError(null)
         const statsData = await getEvaluationTargetsStats(evaluationYear)
-        
+
         if (isMounted) {
           setStats(statsData)
         }
@@ -186,45 +187,59 @@ function EvaluationTargetsStats({ evaluationYear }: { evaluationYear: number })
 export function EvaluationTargetsTable({ promises, evaluationYear, className }: EvaluationTargetsTableProps) {
   const [rowAction, setRowAction] = React.useState<DataTableRowAction<EvaluationTargetWithDepartments> | null>(null)
   const [isFilterPanelOpen, setIsFilterPanelOpen] = React.useState(false)
-  console.count("E Targets render");
   const router = useRouter()
   const searchParams = useSearchParams()
 
   const containerRef = React.useRef<HTMLDivElement>(null)
   const [containerTop, setContainerTop] = React.useState(0)
 
+  // ✅ 스크롤 이벤트 throttling으로 성능 최적화
   const updateContainerBounds = React.useCallback(() => {
     if (containerRef.current) {
       const rect = containerRef.current.getBoundingClientRect()
-      setContainerTop(rect.top)
+      const newTop = rect.top
+
+      // ✅ 값이 실제로 변경될 때만 상태 업데이트
+      setContainerTop(prevTop => {
+        if (Math.abs(prevTop - newTop) > 1) { // 1px 이상 차이날 때만 업데이트
+          return newTop
+        }
+        return prevTop
+      })
     }
   }, [])
 
+  // ✅ throttle 함수 추가
+  const throttledUpdateBounds = React.useCallback(() => {
+    let timeoutId: NodeJS.Timeout
+    return () => {
+      clearTimeout(timeoutId)
+      timeoutId = setTimeout(updateContainerBounds, 16) // ~60fps
+    }
+  }, [updateContainerBounds])
+
   React.useEffect(() => {
     updateContainerBounds()
-    
+
+    const throttledHandler = throttledUpdateBounds()
+
     const handleResize = () => {
       updateContainerBounds()
     }
-    
+
     window.addEventListener('resize', handleResize)
-    window.addEventListener('scroll', updateContainerBounds)
-    
+    window.addEventListener('scroll', throttledHandler) // ✅ throttled 함수 사용
+
     return () => {
       window.removeEventListener('resize', handleResize)
-      window.removeEventListener('scroll', updateContainerBounds)
+      window.removeEventListener('scroll', throttledHandler)
     }
-  }, [updateContainerBounds])
+  }, [updateContainerBounds, throttledUpdateBounds])
 
   const [promiseData] = React.use(promises)
   const tableData = promiseData
 
-  console.log("Evaluation Targets Table Data:", {
-    dataLength: tableData.data?.length,
-    pageCount: tableData.pageCount,
-    total: tableData.total,
-    sampleData: tableData.data?.[0]
-  })
+  console.log(tableData)
 
   const initialSettings = React.useMemo(() => ({
     page: parseInt(searchParams.get('page') || '1'),
@@ -232,7 +247,7 @@ export function EvaluationTargetsTable({ promises, evaluationYear, className }:
     sort: searchParams.get('sort') ? JSON.parse(searchParams.get('sort')!) : [{ id: "createdAt", desc: true }],
     filters: searchParams.get('filters') ? JSON.parse(searchParams.get('filters')!) : [],
     joinOperator: (searchParams.get('joinOperator') as "and" | "or") || "and",
-    basicFilters: searchParams.get('basicFilters') ? 
+    basicFilters: searchParams.get('basicFilters') ?
       JSON.parse(searchParams.get('basicFilters')!) : [],
     basicJoinOperator: (searchParams.get('basicJoinOperator') as "and" | "or") || "and",
     search: searchParams.get('search') || '',
@@ -259,8 +274,8 @@ export function EvaluationTargetsTable({ promises, evaluationYear, className }:
   } = useTablePresets<EvaluationTargetWithDepartments>('evaluation-targets-table', initialSettings)
 
   const columns = React.useMemo(
-    () => getEvaluationTargetsColumns(),
-    []
+    () => getEvaluationTargetsColumns({ setRowAction }),
+    [setRowAction]
   )
 
   const filterFields: DataTableFilterField<EvaluationTargetWithDepartments>[] = [
@@ -271,31 +286,41 @@ export function EvaluationTargetsTable({ promises, evaluationYear, className }:
 
   const advancedFilterFields: DataTableAdvancedFilterField<EvaluationTargetWithDepartments>[] = [
     { id: "evaluationYear", label: "평가년도", type: "number" },
-    { id: "division", label: "구분", type: "select", options: [
-      { label: "해양", value: "OCEAN" },
-      { label: "조선", value: "SHIPYARD" },
-    ]},
+    {
+      id: "division", label: "구분", type: "select", options: [
+        { label: "해양", value: "OCEAN" },
+        { label: "조선", value: "SHIPYARD" },
+      ]
+    },
     { id: "vendorCode", label: "벤더 코드", type: "text" },
     { id: "vendorName", label: "벤더명", type: "text" },
-    { id: "domesticForeign", label: "내외자", type: "select", options: [
-      { label: "내자", value: "DOMESTIC" },
-      { label: "외자", value: "FOREIGN" },
-    ]},
-    { id: "materialType", label: "자재구분", type: "select", options: [
-      { label: "기자재", value: "EQUIPMENT" },
-      { label: "벌크", value: "BULK" },
-      { label: "기자재/벌크", value: "EQUIPMENT_BULK" },
-    ]},
-    { id: "status", label: "상태", type: "select", options: [
-      { label: "검토 중", value: "PENDING" },
-      { label: "확정", value: "CONFIRMED" },
-      { label: "제외", value: "EXCLUDED" },
-    ]},
-    { id: "consensusStatus", label: "의견 일치", type: "select", options: [
-      { label: "의견 일치", value: "true" },
-      { label: "의견 불일치", value: "false" },
-      { label: "검토 중", value: "null" },
-    ]},
+    {
+      id: "domesticForeign", label: "내외자", type: "select", options: [
+        { label: "내자", value: "DOMESTIC" },
+        { label: "외자", value: "FOREIGN" },
+      ]
+    },
+    {
+      id: "materialType", label: "자재구분", type: "select", options: [
+        { label: "기자재", value: "EQUIPMENT" },
+        { label: "벌크", value: "BULK" },
+        { label: "기자재/벌크", value: "EQUIPMENT_BULK" },
+      ]
+    },
+    {
+      id: "status", label: "상태", type: "select", options: [
+        { label: "검토 중", value: "PENDING" },
+        { label: "확정", value: "CONFIRMED" },
+        { label: "제외", value: "EXCLUDED" },
+      ]
+    },
+    {
+      id: "consensusStatus", label: "의견 일치", type: "select", options: [
+        { label: "의견 일치", value: "true" },
+        { label: "의견 불일치", value: "false" },
+        { label: "검토 중", value: "null" },
+      ]
+    },
     { id: "adminComment", label: "관리자 의견", type: "text" },
     { id: "consolidatedComment", label: "종합 의견", type: "text" },
     { id: "confirmedAt", label: "확정일", type: "date" },
@@ -305,17 +330,21 @@ export function EvaluationTargetsTable({ promises, evaluationYear, className }:
   const currentSettings = useMemo(() => {
     return getCurrentSettings()
   }, [getCurrentSettings])
-  
+
+  function getColKey<T>(c: ColumnDef<T>): string | undefined {
+    if ("accessorKey" in c && c.accessorKey) return c.accessorKey as string
+    if ("id" in c && c.id) return c.id as string
+    return undefined
+  }
+
   const initialState = useMemo(() => {
     return {
-      sorting: initialSettings.sort.filter(sortItem => {
-        const columnExists = columns.some(col => col.accessorKey === sortItem.id || col.id === sortItem.id)
-        return columnExists
-      }) as any,
+      sorting: initialSettings.sort.filter(s =>
+        columns.some(c => getColKey(c) === s.id)),
       columnVisibility: currentSettings.columnVisibility,
       columnPinning: currentSettings.pinnedColumns,
     }
-  }, [currentSettings, initialSettings.sort, columns])
+  }, [columns, currentSettings, initialSettings.sort])
 
   const { table } = useDataTable({
     data: tableData.data,
@@ -349,12 +378,12 @@ export function EvaluationTargetsTable({ promises, evaluationYear, className }:
   return (
     <>
       {/* Filter Panel */}
-      <div 
+      <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={{ 
+        style={{
           width: isFilterPanelOpen ? `${FILTER_PANEL_WIDTH}px` : '0px',
           top: `${containerTop}px`,
           height: `calc(100vh - ${containerTop}px)`
@@ -362,7 +391,7 @@ export function EvaluationTargetsTable({ promises, evaluationYear, className }:
       >
         <div className="h-full">
           <EvaluationTargetFilterSheet
-            isOpen={isFilterPanelOpen} 
+            isOpen={isFilterPanelOpen}
             onClose={() => setIsFilterPanelOpen(false)}
             onSearch={handleSearch}
             isLoading={false}
@@ -371,12 +400,12 @@ export function EvaluationTargetsTable({ promises, evaluationYear, className }:
       </div>
 
       {/* Main Content Container */}
-      <div 
+      <div
         ref={containerRef}
         className={cn("relative w-full overflow-hidden", className)}
       >
         <div className="flex w-full h-full">
-          <div 
+          <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%',
@@ -386,14 +415,14 @@ export function EvaluationTargetsTable({ promises, evaluationYear, className }:
             {/* 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" 
+                <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"/>}
+                  {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()}
@@ -401,7 +430,7 @@ export function EvaluationTargetsTable({ promises, evaluationYear, className }:
                   )}
                 </Button>
               </div>
-              
+
               <div className="text-sm text-muted-foreground">
                 {tableData && (
                   <span>총 {tableData.total || tableData.data.length}건</span>
@@ -437,11 +466,18 @@ export function EvaluationTargetsTable({ promises, evaluationYear, className }:
                         onSetDefaultPreset={setDefaultPreset}
                         onRenamePreset={renamePreset}
                       />
-                      
+
                       <EvaluationTargetsTableToolbarActions table={table} />
                     </div>
                   </DataTableAdvancedToolbar>
                 </DataTable>
+
+                <EditEvaluationTargetSheet
+                  open={rowAction?.type === "update"}
+                  onOpenChange={() => setRowAction(null)}
+                  evaluationTarget={rowAction?.row.original ?? null}
+                />
+
               </div>
             </div>
           </div>
-- 
cgit v1.2.3

