diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-06-20 11:37:31 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-06-20 11:37:31 +0000 |
| commit | aa86729f9a2ab95346a2851e3837de1c367aae17 (patch) | |
| tree | b601b18b6724f2fb449c7fa9ea50cbd652a8077d /lib/evaluation-target-list/table/evaluation-target-table.tsx | |
| parent | 95bbe9c583ff841220da1267630e7b2025fc36dc (diff) | |
(대표님) 20250620 작업사항
Diffstat (limited to 'lib/evaluation-target-list/table/evaluation-target-table.tsx')
| -rw-r--r-- | lib/evaluation-target-list/table/evaluation-target-table.tsx | 154 |
1 files changed, 95 insertions, 59 deletions
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> |
