diff options
Diffstat (limited to 'lib/evaluation-target-list/table/evaluation-target-table.tsx')
| -rw-r--r-- | lib/evaluation-target-list/table/evaluation-target-table.tsx | 109 |
1 files changed, 93 insertions, 16 deletions
diff --git a/lib/evaluation-target-list/table/evaluation-target-table.tsx b/lib/evaluation-target-list/table/evaluation-target-table.tsx index 87be3589..b140df0e 100644 --- a/lib/evaluation-target-list/table/evaluation-target-table.tsx +++ b/lib/evaluation-target-list/table/evaluation-target-table.tsx @@ -7,7 +7,7 @@ import * as React from "react"; import { useSearchParams } from "next/navigation"; import { Button } from "@/components/ui/button"; -import { PanelLeftClose, PanelLeftOpen } from "lucide-react"; +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"; @@ -28,6 +28,74 @@ import { EvaluationTargetsTableToolbarActions } from "./evaluation-targets-toolb import { EvaluationTargetFilterSheet } from "./evaluation-targets-filter-sheet"; import { EvaluationTargetWithDepartments } from "@/db/schema"; import { EditEvaluationTargetSheet } from "./update-evaluation-target"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; + +/* -------------------------------------------------------------------------- */ +/* Process Guide Popover */ +/* -------------------------------------------------------------------------- */ +function ProcessGuidePopover() { + return ( + <Popover> + <PopoverTrigger asChild> + <Button variant="ghost" size="icon" className="h-6 w-6"> + <HelpCircle className="h-4 w-4 text-muted-foreground" /> + </Button> + </PopoverTrigger> + <PopoverContent className="w-96" align="start"> + <div className="space-y-3"> + <div className="space-y-1"> + <h4 className="font-medium">평가 대상 확정 프로세스</h4> + <p className="text-sm text-muted-foreground"> + 발주실적을 기반으로 평가 대상을 확정하는 절차입니다. + </p> + </div> + <div className="space-y-3 text-sm"> + <div className="flex gap-3"> + <div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100 text-xs font-medium text-blue-600"> + 1 + </div> + <div> + <p className="font-medium">발주실적 기반 자동 추출</p> + <p className="text-muted-foreground">전년도 10월 ~ 해당년도 9월 발주실적에서 업체 목록을 자동으로 생성합니다.</p> + </div> + </div> + <div className="flex gap-3"> + <div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100 text-xs font-medium text-blue-600"> + 2 + </div> + <div> + <p className="font-medium">담당자 지정</p> + <p className="text-muted-foreground">각 평가 대상별로 5개 부서(발주/조달/품질/설계/CS)의 담당자를 지정합니다.</p> + </div> + </div> + <div className="flex gap-3"> + <div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100 text-xs font-medium text-blue-600"> + 3 + </div> + <div> + <p className="font-medium">검토 및 의견 수렴</p> + <p className="text-muted-foreground">모든 담당자가 평가 대상 적합성을 검토하고 의견을 제출합니다.</p> + </div> + </div> + <div className="flex gap-3"> + <div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100 text-xs font-medium text-blue-600"> + 4 + </div> + <div> + <p className="font-medium">최종 확정</p> + <p className="text-muted-foreground">모든 담당자 의견이 일치하면 평가 대상으로 최종 확정됩니다.</p> + </div> + </div> + </div> + </div> + </PopoverContent> + </Popover> + ) +} /* -------------------------------------------------------------------------- */ /* Stats Card */ @@ -130,7 +198,7 @@ function EvaluationTargetsStats({ evaluationYear }: { evaluationYear: number }) <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> + <Badge variant="success">완료</Badge> </CardHeader> <CardContent> <div className="text-2xl font-bold text-green-600">{confirmed.toLocaleString()}</div> @@ -204,10 +272,17 @@ export function EvaluationTargetsTable({ promises, evaluationYear, className }: const tableData = promiseData; /* ---------------------- 검색 파라미터 안전 처리 ---------------------- */ - const getSearchParam = React.useCallback((key: string, defaultValue?: string): string => { - return searchParams?.get(key) ?? defaultValue ?? ""; - }, [searchParams]); - + const searchString = React.useMemo( + () => searchParams.toString(), // query가 바뀔 때만 새로 계산 + [searchParams] + ); + + const getSearchParam = React.useCallback( + (key: string, def = "") => + new URLSearchParams(searchString).get(key) ?? def, + [searchString] + ); + // 제네릭 함수는 useCallback 밖에서 정의 const parseSearchParamHelper = React.useCallback((key: string, defaultValue: any): any => { try { @@ -226,7 +301,7 @@ const parseSearchParam = <T,>(key: string, defaultValue: T): T => { const initialSettings = React.useMemo(() => ({ page: parseInt(getSearchParam("page", "1")), perPage: parseInt(getSearchParam("perPage", "10")), - sort: parseSearchParam("sort", [{ id: "createdAt", desc: true }]), + sort: getSearchParam('sort') ? JSON.parse(getSearchParam('sort')!) : [{ id: "createdAt", desc: true }], filters: parseSearchParam("filters", []), joinOperator: (getSearchParam("joinOperator") as "and" | "or") || "and", basicFilters: parseSearchParam("basicFilters", []), @@ -237,7 +312,7 @@ const parseSearchParam = <T,>(key: string, defaultValue: T): T => { pinnedColumns: { left: [], right: ["actions"] }, groupBy: [], expandedRows: [], - }), [getSearchParam, parseSearchParamHelper]); + }), [getSearchParam]); /* --------------------- 프리셋 훅 ------------------------------ */ const { @@ -267,9 +342,6 @@ const parseSearchParam = <T,>(key: string, defaultValue: T): T => { // { accessorKey: "division", header: "구분" } // ]; -window.addEventListener('beforeunload', () => { - console.trace('[beforeunload] 문서가 통째로 사라지려 합니다!'); -}); /* 기본 필터 */ const filterFields: DataTableFilterField<EvaluationTargetWithDepartments>[] = [ @@ -297,11 +369,16 @@ window.addEventListener('beforeunload', () => { /* current settings */ const currentSettings = React.useMemo(() => getCurrentSettings(), [getCurrentSettings]); - const initialState = React.useMemo(() => ({ - sorting: initialSettings.sort.filter((s: any) => columns.some((c: any) => ("accessorKey" in c ? c.accessorKey : c.id) === s.id)), - columnVisibility: currentSettings.columnVisibility, - columnPinning: currentSettings.pinnedColumns, - }), [columns, currentSettings, initialSettings.sort]); + 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({ |
