summaryrefslogtreecommitdiff
path: root/lib/legal-review/status/legal-table.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/legal-review/status/legal-table.tsx')
-rw-r--r--lib/legal-review/status/legal-table.tsx546
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