diff options
Diffstat (limited to 'lib/legal-review/status/legal-table.tsx')
| -rw-r--r-- | lib/legal-review/status/legal-table.tsx | 546 |
1 files changed, 0 insertions, 546 deletions
diff --git a/lib/legal-review/status/legal-table.tsx b/lib/legal-review/status/legal-table.tsx deleted file mode 100644 index 4df3568c..00000000 --- a/lib/legal-review/status/legal-table.tsx +++ /dev/null @@ -1,546 +0,0 @@ -// ============================================================================ -// components/evaluation-targets-table.tsx (CLIENT COMPONENT) -// ─ 정리된 버전 ─ -// ============================================================================ -"use client"; - -import * as React from "react"; -import { useSearchParams } from "next/navigation"; -import { Button } from "@/components/ui/button"; -import { HelpCircle, PanelLeftClose, PanelLeftOpen } from "lucide-react"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; -import { Skeleton } from "@/components/ui/skeleton"; -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 { cn } from "@/lib/utils"; -import { useTablePresets } from "@/components/data-table/use-table-presets"; -import { TablePresetManager } from "@/components/data-table/data-table-preset"; -import { LegalWorksDetailView } from "@/db/schema"; -import { LegalWorksTableToolbarActions } from "./legal-works-toolbar-actions"; -import { getLegalWorks } from "../service"; -import { getLegalWorksColumns } from "./legal-works-columns"; -import { LegalWorkFilterSheet } from "./legal-work-filter-sheet"; -import { EditLegalWorkSheet } from "./update-legal-work-dialog"; -import { LegalWorkDetailDialog } from "./legal-work-detail-dialog"; -import { DeleteLegalWorksDialog } from "./delete-legal-works-dialog"; - - -/* -------------------------------------------------------------------------- */ -/* Stats Card */ -/* -------------------------------------------------------------------------- */ -function LegalWorksStats({ data }: { data: LegalWorksDetailView[] }) { - const stats = React.useMemo(() => { - const total = data.length; - const pending = data.filter(item => item.status === '검토요청').length; - const assigned = data.filter(item => item.status === '담당자배정').length; - const inProgress = data.filter(item => item.status === '검토중').length; - const completed = data.filter(item => item.status === '답변완료').length; - const urgent = data.filter(item => item.isUrgent).length; - - return { total, pending, assigned, inProgress, completed, urgent }; - }, [data]); - - if (stats.total === 0) { - return ( - <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-5 mb-6"> - <Card className="col-span-full"> - <CardContent className="pt-6 text-center text-sm text-muted-foreground"> - 등록된 법무업무가 없습니다. - </CardContent> - </Card> - </div> - ); - } - - return ( - <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-5 mb-6"> - <Card> - <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> - <CardTitle className="text-sm font-medium">총 건수</CardTitle> - <Badge variant="outline">전체</Badge> - </CardHeader> - <CardContent> - <div className="text-2xl font-bold">{stats.total.toLocaleString()}</div> - <div className="text-xs text-muted-foreground mt-1"> - 긴급 {stats.urgent}건 - </div> - </CardContent> - </Card> - - <Card> - <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> - <CardTitle className="text-sm font-medium">검토요청</CardTitle> - <Badge variant="secondary">대기</Badge> - </CardHeader> - <CardContent> - <div className="text-2xl font-bold text-blue-600">{stats.pending.toLocaleString()}</div> - <div className="text-xs text-muted-foreground mt-1"> - {stats.total ? Math.round((stats.pending / stats.total) * 100) : 0}% of total - </div> - </CardContent> - </Card> - - <Card> - <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> - <CardTitle className="text-sm font-medium">담당자배정</CardTitle> - <Badge variant="secondary">진행</Badge> - </CardHeader> - <CardContent> - <div className="text-2xl font-bold text-yellow-600">{stats.assigned.toLocaleString()}</div> - <div className="text-xs text-muted-foreground mt-1"> - {stats.total ? Math.round((stats.assigned / stats.total) * 100) : 0}% of total - </div> - </CardContent> - </Card> - - <Card> - <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> - <CardTitle className="text-sm font-medium">검토중</CardTitle> - <Badge variant="secondary">진행</Badge> - </CardHeader> - <CardContent> - <div className="text-2xl font-bold text-orange-600">{stats.inProgress.toLocaleString()}</div> - <div className="text-xs text-muted-foreground mt-1"> - {stats.total ? Math.round((stats.inProgress / stats.total) * 100) : 0}% of total - </div> - </CardContent> - </Card> - - <Card> - <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> - <CardTitle className="text-sm font-medium">답변완료</CardTitle> - <Badge variant="default">완료</Badge> - </CardHeader> - <CardContent> - <div className="text-2xl font-bold text-green-600">{stats.completed.toLocaleString()}</div> - <div className="text-xs text-muted-foreground mt-1"> - {stats.total ? Math.round((stats.completed / stats.total) * 100) : 0}% of total - </div> - </CardContent> - </Card> - </div> - ); -} - -/* -------------------------------------------------------------------------- */ -/* EvaluationTargetsTable */ -/* -------------------------------------------------------------------------- */ -interface LegalWorksTableProps { - promises: Promise<[Awaited<ReturnType<typeof getLegalWorks>>]>; - currentYear: number; - className?: string; -} - -export function LegalWorksTable({ promises, currentYear = new Date().getFullYear(), className }: LegalWorksTableProps) { - const [rowAction, setRowAction] = React.useState<DataTableRowAction<LegalWorksDetailView> | null>(null); - const [isFilterPanelOpen, setIsFilterPanelOpen] = React.useState(false); - const searchParams = useSearchParams(); - - // ✅ 외부 필터 상태 (폼에서 전달받은 필터) - const [externalFilters, setExternalFilters] = React.useState<any[]>([]); - const [externalJoinOperator, setExternalJoinOperator] = React.useState<"and" | "or">("and"); - - // ✅ 폼에서 전달받은 필터를 처리하는 핸들러 - const handleFiltersApply = React.useCallback((filters: any[], joinOperator: "and" | "or") => { - console.log("=== 폼에서 필터 전달받음 ===", filters, joinOperator); - setExternalFilters(filters); - setExternalJoinOperator(joinOperator); - // 필터 적용 후 패널 닫기 - setIsFilterPanelOpen(false); - }, []); - - - const searchString = React.useMemo( - () => searchParams.toString(), - [searchParams] - ); - - const getSearchParam = React.useCallback( - (key: string, def = "") => - new URLSearchParams(searchString).get(key) ?? def, - [searchString] - ); - - - // ✅ URL 필터 변경 감지 및 데이터 새로고침 - React.useEffect(() => { - const refetchData = async () => { - try { - setIsDataLoading(true); - - // 현재 URL 파라미터 기반으로 새 검색 파라미터 생성 - const currentFilters = getSearchParam("filters"); - const currentJoinOperator = getSearchParam("joinOperator", "and"); - const currentPage = parseInt(getSearchParam("page", "1")); - const currentPerPage = parseInt(getSearchParam("perPage", "10")); - const currentSort = getSearchParam('sort') ? JSON.parse(getSearchParam('sort')!) : [{ id: "createdAt", desc: true }]; - const currentSearch = getSearchParam("search", ""); - - const searchParams = { - filters: currentFilters ? JSON.parse(currentFilters) : [], - joinOperator: currentJoinOperator as "and" | "or", - page: currentPage, - perPage: currentPerPage, - sort: currentSort, - search: currentSearch, - currentYear: currentYear - }; - - console.log("=== 새 데이터 요청 ===", searchParams); - - // 서버 액션 직접 호출 - const newData = await getLegalWorks(searchParams); - setTableData(newData); - - console.log("=== 데이터 업데이트 완료 ===", newData.data.length, "건"); - } catch (error) { - console.error("데이터 새로고침 오류:", error); - } finally { - setIsDataLoading(false); - } - }; - - /* ---------------------- 검색 파라미터 안전 처리 ---------------------- */ - - // 필터나 검색 파라미터가 변경되면 데이터 새로고침 (디바운스 적용) - const timeoutId = setTimeout(() => { - // 필터, 검색, 페이지네이션, 정렬 중 하나라도 변경되면 새로고침 - const hasChanges = getSearchParam("filters") || - getSearchParam("search") || - getSearchParam("page") !== "1" || - getSearchParam("perPage") !== "10" || - getSearchParam("sort"); - - if (hasChanges) { - refetchData(); - } - }, 300); // 디바운스 시간 단축 - - return () => clearTimeout(timeoutId); - }, [searchString, currentYear, getSearchParam]); - - const refreshData = React.useCallback(async () => { - try { - setIsDataLoading(true); - - // 현재 URL 파라미터로 데이터 새로고침 - const currentFilters = getSearchParam("filters"); - const currentJoinOperator = getSearchParam("joinOperator", "and"); - const currentPage = parseInt(getSearchParam("page", "1")); - const currentPerPage = parseInt(getSearchParam("perPage", "10")); - const currentSort = getSearchParam('sort') ? JSON.parse(getSearchParam('sort')!) : [{ id: "createdAt", desc: true }]; - const currentSearch = getSearchParam("search", ""); - - const searchParams = { - filters: currentFilters ? JSON.parse(currentFilters) : [], - joinOperator: currentJoinOperator as "and" | "or", - page: currentPage, - perPage: currentPerPage, - sort: currentSort, - search: currentSearch, - currentYear: currentYear - }; - - const newData = await getLegalWorks(searchParams); - setTableData(newData); - - console.log("=== 데이터 새로고침 완료 ===", newData.data.length, "건"); - } catch (error) { - console.error("데이터 새로고침 오류:", error); - } finally { - setIsDataLoading(false); - } - }, [currentYear, getSearchParam]); - - /* --------------------------- layout refs --------------------------- */ - const containerRef = React.useRef<HTMLDivElement>(null); - const [containerTop, setContainerTop] = React.useState(0); - - const updateContainerBounds = React.useCallback(() => { - if (containerRef.current) { - const rect = containerRef.current.getBoundingClientRect() - const newTop = rect.top - setContainerTop(prevTop => { - if (Math.abs(prevTop - newTop) > 1) { // 1px 이상 차이날 때만 업데이트 - return newTop - } - return prevTop - }) - } - }, []) - React.useEffect(() => { - updateContainerBounds(); - - const handleResize = () => { - updateContainerBounds(); - }; - - window.addEventListener('resize', handleResize); - window.addEventListener('scroll', updateContainerBounds); - - return () => { - window.removeEventListener('resize', handleResize); - window.removeEventListener('scroll', updateContainerBounds); - }; - }, [updateContainerBounds]); - - /* ---------------------- 데이터 상태 관리 ---------------------- */ - // 초기 데이터 설정 - const [initialPromiseData] = React.use(promises); - - // ✅ 테이블 데이터 상태 추가 - const [tableData, setTableData] = React.useState(initialPromiseData); - const [isDataLoading, setIsDataLoading] = React.useState(false); - - const parseSearchParamHelper = React.useCallback((key: string, defaultValue: any): any => { - try { - const value = getSearchParam(key); - return value ? JSON.parse(value) : defaultValue; - } catch { - return defaultValue; - } - }, [getSearchParam]); - - const parseSearchParam = <T,>(key: string, defaultValue: T): T => { - return parseSearchParamHelper(key, defaultValue); - }; - - /* ---------------------- 초기 설정 ---------------------------- */ - const initialSettings = React.useMemo(() => ({ - page: parseInt(getSearchParam("page", "1")), - perPage: parseInt(getSearchParam("perPage", "10")), - sort: getSearchParam('sort') ? JSON.parse(getSearchParam('sort')!) : [{ id: "createdAt", desc: true }], - filters: parseSearchParam("filters", []), - joinOperator: (getSearchParam("joinOperator") as "and" | "or") || "and", - search: getSearchParam("search", ""), - columnVisibility: {}, - columnOrder: [], - pinnedColumns: { left: [], right: ["actions"] }, - groupBy: [], - expandedRows: [], - }), [getSearchParam, parseSearchParam]); - - /* --------------------- 프리셋 훅 ------------------------------ */ - const { - presets, - activePresetId, - hasUnsavedChanges, - isLoading: presetsLoading, - createPreset, - applyPreset, - updatePreset, - deletePreset, - setDefaultPreset, - renamePreset, - getCurrentSettings, - } = useTablePresets<LegalWorksDetailView>( - "legal-review-table", - initialSettings - ); - - - - /* --------------------- 컬럼 ------------------------------ */ - const columns = React.useMemo(() => getLegalWorksColumns({ setRowAction }), [setRowAction]); - - /* 기본 필터 */ - const filterFields: DataTableFilterField<LegalWorksDetailView>[] = [ - { id: "vendorCode", label: "벤더 코드" }, - { id: "vendorName", label: "벤더명" }, - { id: "status", label: "상태" }, - ]; - - /* 고급 필터 */ - const advancedFilterFields: DataTableAdvancedFilterField<LegalWorksDetailView>[] = [ - ]; - - /* current settings */ - const currentSettings = React.useMemo(() => getCurrentSettings(), [getCurrentSettings]); - - const initialState = React.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]) - - /* ----------------------- useDataTable ------------------------ */ - const { table } = useDataTable({ - data: tableData.data, - columns, - pageCount: tableData.pageCount, - rowCount: tableData.total || tableData.data.length, - filterFields, - enablePinning: true, - enableAdvancedFilter: true, - initialState, - getRowId: (row) => String(row.id), - shallow: false, - clearOnDefault: true, - }); - - /* ---------------------- helper ------------------------------ */ - const getActiveFilterCount = React.useCallback(() => { - try { - // URL에서 현재 필터 수 확인 - const filtersParam = getSearchParam("filters"); - if (filtersParam) { - const filters = JSON.parse(filtersParam); - return Array.isArray(filters) ? filters.length : 0; - } - return 0; - } catch { - return 0; - } - }, [getSearchParam]); - - const FILTER_PANEL_WIDTH = 400; - - /* ---------------------------- JSX ---------------------------- */ - return ( - <> - {/* Filter Panel */} - <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)` - }} - > - <LegalWorkFilterSheet - isOpen={isFilterPanelOpen} - onClose={() => setIsFilterPanelOpen(false)} - onFiltersApply={handleFiltersApply} // ✅ 필터 적용 콜백 전달 - isLoading={false} - /> - </div> - - {/* Main Container */} - <div ref={containerRef} className={cn("relative w-full overflow-hidden", className)}> - <div className="flex w-full h-full"> - <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 */} - <div className="flex items-center justify-between p-4 bg-background shrink-0"> - <Button - variant="outline" - size="sm" - onClick={() => setIsFilterPanelOpen(!isFilterPanelOpen)} - className="flex items-center shadow-sm" - > - {isFilterPanelOpen ? <PanelLeftClose className="size-4" /> : <PanelLeftOpen className="size-4" />} - {getActiveFilterCount() > 0 && ( - <span className="ml-2 bg-primary text-primary-foreground rounded-full px-2 py-0.5 text-xs"> - {getActiveFilterCount()} - </span> - )} - </Button> - <div className="text-sm text-muted-foreground"> - 총 {tableData.total || tableData.data.length}건 - </div> - </div> - - {/* Stats */} - <div className="px-4"> - <LegalWorksStats data={tableData.data} /> - - </div> - - {/* Table */} - <div className="flex-1 overflow-hidden relative" style={{ height: "calc(100vh - 500px)" }}> - {isDataLoading && ( - <div className="absolute inset-0 bg-background/50 backdrop-blur-sm z-10 flex items-center justify-center"> - <div className="flex items-center gap-2 text-sm text-muted-foreground"> - <div className="w-4 h-4 border-2 border-primary border-t-transparent rounded-full animate-spin" /> - 필터링 중... - </div> - </div> - )} - <DataTable table={table} className="h-full"> - {/* ✅ 확장된 DataTableAdvancedToolbar 사용 */} - <DataTableAdvancedToolbar - table={table} - filterFields={advancedFilterFields} - debounceMs={300} - shallow={false} - externalFilters={externalFilters} - externalJoinOperator={externalJoinOperator} - onFiltersChange={(filters, joinOperator) => { - console.log("=== 필터 변경 감지 ===", filters, joinOperator); - }} - > - <div className="flex items-center gap-2"> - <TablePresetManager<LegalWorksDetailView> - presets={presets} - activePresetId={activePresetId} - currentSettings={currentSettings} - hasUnsavedChanges={hasUnsavedChanges} - isLoading={presetsLoading} - onCreatePreset={createPreset} - onUpdatePreset={updatePreset} - onDeletePreset={deletePreset} - onApplyPreset={applyPreset} - onSetDefaultPreset={setDefaultPreset} - onRenamePreset={renamePreset} - /> - - <LegalWorksTableToolbarActions table={table}onRefresh={refreshData} /> - </div> - </DataTableAdvancedToolbar> - </DataTable> - - {/* 다이얼로그들 */} - <EditLegalWorkSheet - open={rowAction?.type === "update"} - onOpenChange={() => setRowAction(null)} - work={rowAction?.row.original || null} - onSuccess={() => { - rowAction?.row.toggleSelected(false); - refreshData(); - }} - /> - - <LegalWorkDetailDialog - open={rowAction?.type === "view"} - onOpenChange={(open) => !open && setRowAction(null)} - work={rowAction?.row.original || null} - /> - - <DeleteLegalWorksDialog - open={rowAction?.type === "delete"} - onOpenChange={(open) => !open && setRowAction(null)} - legalWorks={rowAction?.row.original ? [rowAction.row.original] : []} - showTrigger={false} - onSuccess={() => { - setRowAction(null); - refreshData(); - }} - /> - - </div> - </div> - </div> - </div> - </> - ); -}
\ No newline at end of file |
