// ============================================================================
// 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 (
등록된 법무업무가 없습니다.
);
}
return (
총 건수
전체
{stats.total.toLocaleString()}
긴급 {stats.urgent}건
검토요청
대기
{stats.pending.toLocaleString()}
{stats.total ? Math.round((stats.pending / stats.total) * 100) : 0}% of total
담당자배정
진행
{stats.assigned.toLocaleString()}
{stats.total ? Math.round((stats.assigned / stats.total) * 100) : 0}% of total
검토중
진행
{stats.inProgress.toLocaleString()}
{stats.total ? Math.round((stats.inProgress / stats.total) * 100) : 0}% of total
답변완료
완료
{stats.completed.toLocaleString()}
{stats.total ? Math.round((stats.completed / stats.total) * 100) : 0}% of total
);
}
/* -------------------------------------------------------------------------- */
/* EvaluationTargetsTable */
/* -------------------------------------------------------------------------- */
interface LegalWorksTableProps {
promises: Promise<[Awaited>]>;
currentYear: number;
className?: string;
}
export function LegalWorksTable({ promises, currentYear = new Date().getFullYear(), className }: LegalWorksTableProps) {
const [rowAction, setRowAction] = React.useState | null>(null);
const [isFilterPanelOpen, setIsFilterPanelOpen] = React.useState(false);
const searchParams = useSearchParams();
// ✅ 외부 필터 상태 (폼에서 전달받은 필터)
const [externalFilters, setExternalFilters] = React.useState([]);
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(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 = (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(
"legal-review-table",
initialSettings
);
/* --------------------- 컬럼 ------------------------------ */
const columns = React.useMemo(() => getLegalWorksColumns({ setRowAction }), [setRowAction]);
/* 기본 필터 */
const filterFields: DataTableFilterField[] = [
{ id: "vendorCode", label: "벤더 코드" },
{ id: "vendorName", label: "벤더명" },
{ id: "status", label: "상태" },
];
/* 고급 필터 */
const advancedFilterFields: DataTableAdvancedFilterField[] = [
];
/* 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 */}
setIsFilterPanelOpen(false)}
onFiltersApply={handleFiltersApply} // ✅ 필터 적용 콜백 전달
isLoading={false}
/>
{/* Main Container */}
{/* Header */}
총 {tableData.total || tableData.data.length}건
{/* Stats */}
{/* Table */}
{isDataLoading && (
)}
{/* ✅ 확장된 DataTableAdvancedToolbar 사용 */}
{
console.log("=== 필터 변경 감지 ===", filters, joinOperator);
}}
>
presets={presets}
activePresetId={activePresetId}
currentSettings={currentSettings}
hasUnsavedChanges={hasUnsavedChanges}
isLoading={presetsLoading}
onCreatePreset={createPreset}
onUpdatePreset={updatePreset}
onDeletePreset={deletePreset}
onApplyPreset={applyPreset}
onSetDefaultPreset={setDefaultPreset}
onRenamePreset={renamePreset}
/>
{/* 다이얼로그들 */}
setRowAction(null)}
work={rowAction?.row.original || null}
onSuccess={() => {
rowAction?.row.toggleSelected(false);
refreshData();
}}
/>
!open && setRowAction(null)}
work={rowAction?.row.original || null}
/>
!open && setRowAction(null)}
legalWorks={rowAction?.row.original ? [rowAction.row.original] : []}
showTrigger={false}
onSuccess={() => {
setRowAction(null);
refreshData();
}}
/>
>
);
}