diff options
Diffstat (limited to 'lib/evaluation-target-list/table/evaluation-targets-columns.tsx')
| -rw-r--r-- | lib/evaluation-target-list/table/evaluation-targets-columns.tsx | 405 |
1 files changed, 191 insertions, 214 deletions
diff --git a/lib/evaluation-target-list/table/evaluation-targets-columns.tsx b/lib/evaluation-target-list/table/evaluation-targets-columns.tsx index b6631f14..60f1af39 100644 --- a/lib/evaluation-target-list/table/evaluation-targets-columns.tsx +++ b/lib/evaluation-target-list/table/evaluation-targets-columns.tsx @@ -9,34 +9,22 @@ import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table- import { EvaluationTargetWithDepartments } from "@/db/schema"; import type { DataTableRowAction } from "@/types/table"; import { formatDate } from "@/lib/utils"; +import { vendortypeMap } from "@/types/evaluation"; interface GetColumnsProps { setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<EvaluationTargetWithDepartments> | null>>; } -// ✅ 모든 헬퍼 함수들을 컴포넌트 외부로 이동 (매번 재생성 방지) +// ✅ 모든 헬퍼 함수들을 컴포넌트 외부로 이동 const getStatusBadgeVariant = (status: string) => { switch (status) { - case "PENDING": - return "secondary"; - case "CONFIRMED": - return "default"; - case "EXCLUDED": - return "destructive"; - default: - return "outline"; + case "PENDING": return "secondary"; + case "CONFIRMED": return "default"; + case "EXCLUDED": return "destructive"; + default: return "outline"; } }; -const getStatusText = (status: string) => { - const statusMap = { - PENDING: "검토 중", - CONFIRMED: "확정", - EXCLUDED: "제외" - }; - return statusMap[status] || status; -}; - const getConsensusBadge = (consensusStatus: boolean | null) => { if (consensusStatus === null) { return <Badge variant="outline">검토 중</Badge>; @@ -56,12 +44,7 @@ const getDivisionBadge = (division: string) => { }; const getMaterialTypeBadge = (materialType: string) => { - const typeMap = { - EQUIPMENT: "기자재", - BULK: "벌크", - EQUIPMENT_BULK: "기자재/벌크" - }; - return <Badge variant="outline">{typeMap[materialType] || materialType}</Badge>; + return <Badge variant="outline">{vendortypeMap[materialType] || materialType}</Badge>; }; const getDomesticForeignBadge = (domesticForeign: string) => { @@ -72,7 +55,6 @@ const getDomesticForeignBadge = (domesticForeign: string) => { ); }; -// ✅ 평가 대상 여부 표시 함수 const getEvaluationTargetBadge = (isTarget: boolean | null) => { if (isTarget === null) { return <Badge variant="outline">미정</Badge>; @@ -90,340 +72,335 @@ const getEvaluationTargetBadge = (isTarget: boolean | null) => { ); }; -export function getEvaluationTargetsColumns({ setRowAction }: GetColumnsProps): ColumnDef<EvaluationTargetWithDepartments>[] { +// ✅ 모든 cell 렌더러 함수들을 미리 정의 (매번 새로 생성 방지) +const renderEvaluationYear = ({ row }: any) => ( + <span className="font-medium">{row.getValue("evaluationYear")}</span> +); + +const renderDivision = ({ row }: any) => getDivisionBadge(row.getValue("division")); + +const renderStatus = ({ row }: any) => { + const status = row.getValue<string>("status"); + return ( + <Badge variant={getStatusBadgeVariant(status)}> + {status} + </Badge> + ); +}; + +const renderConsensusStatus = ({ row }: any) => getConsensusBadge(row.getValue("consensusStatus")); + +const renderVendorCode = ({ row }: any) => ( + <span className="font-mono text-sm">{row.getValue("vendorCode")}</span> +); + +const renderVendorName = ({ row }: any) => ( + <div className="truncate max-w-[200px]" title={row.getValue<string>("vendorName")!}> + {row.getValue("vendorName") as string} + </div> +); + +const renderDomesticForeign = ({ row }: any) => getDomesticForeignBadge(row.getValue("domesticForeign")); + +const renderMaterialType = ({ row }: any) => getMaterialTypeBadge(row.getValue("materialType")); + +const renderReviewerName = (fieldName: string) => ({ row }: any) => { + const reviewerName = row.getValue<string>(fieldName); + return reviewerName ? ( + <div className="truncate max-w-[120px]" title={reviewerName}> + {reviewerName} + </div> + ) : ( + <span className="text-muted-foreground">-</span> + ); +}; + +const renderIsApproved = (fieldName: string) => ({ row }: any) => { + const isApproved = row.getValue<boolean>(fieldName); + return getEvaluationTargetBadge(isApproved); +}; + +const renderComment = (maxWidth: string) => ({ row }: any) => { + const comment = row.getValue<string>("adminComment") || row.getValue<string>("consolidatedComment"); + return comment ? ( + <div className={`truncate ${maxWidth}`} title={comment}> + {comment} + </div> + ) : ( + <span className="text-muted-foreground">-</span> + ); +}; + +const renderConfirmedAt = ({ row }: any) => { + const confirmedAt = row.getValue<Date>("confirmedAt"); + return <span className="text-sm">{confirmedAt ? formatDate(confirmedAt, "KR") : '-'}</span>; +}; + +const renderCreatedAt = ({ row }: any) => { + const createdAt = row.getValue<Date>("createdAt"); + return <span className="text-sm">{formatDate(createdAt, "KR")}</span>; +}; + +// ✅ 헤더 렌더러들도 미리 정의 +const createHeaderRenderer = (title: string) => ({ column }: any) => ( + <DataTableColumnHeaderSimple column={column} title={title} /> +); + +// ✅ 체크박스 관련 함수들도 미리 정의 +const renderSelectAllCheckbox = ({ table }: any) => ( + <Checkbox + checked={table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && "indeterminate")} + onCheckedChange={(v: any) => table.toggleAllPageRowsSelected(!!v)} + aria-label="select all" + className="translate-y-0.5" + /> +); + +const renderRowCheckbox = ({ row }: any) => ( + <Checkbox + checked={row.getIsSelected()} + onCheckedChange={(v: any) => row.toggleSelected(!!v)} + aria-label="select row" + className="translate-y-0.5" + /> +); + +// ✅ 정적 컬럼 정의 (setRowAction만 동적으로 주입) +function createStaticColumns(setRowAction: GetColumnsProps['setRowAction']): ColumnDef<EvaluationTargetWithDepartments>[] { + // Actions 컬럼의 클릭 핸들러를 미리 정의 + const renderActionsCell = ({ row }: any) => ( + <div className="flex items-center gap-1"> + <Button + variant="ghost" + size="icon" + className="size-8" + onClick={() => setRowAction({ row, type: "update" })} + aria-label="수정" + title="수정" + > + <Pencil className="size-4" /> + </Button> + </div> + ); + return [ - // ✅ Checkbox + // Checkbox { id: "select", - header: ({ table }) => ( - <Checkbox - checked={table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && "indeterminate")} - onCheckedChange={(v) => table.toggleAllPageRowsSelected(!!v)} - aria-label="select all" - className="translate-y-0.5" - /> - ), - cell: ({ row }) => ( - <Checkbox - checked={row.getIsSelected()} - onCheckedChange={(v) => row.toggleSelected(!!v)} - aria-label="select row" - className="translate-y-0.5" - /> - ), + header: renderSelectAllCheckbox, + cell: renderRowCheckbox, size: 40, enableSorting: false, enableHiding: false, }, - // ✅ 기본 정보 + // 기본 정보 { accessorKey: "evaluationYear", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="평가년도" />, - cell: ({ row }) => <span className="font-medium">{row.getValue("evaluationYear")}</span>, + header: createHeaderRenderer("평가년도"), + cell: renderEvaluationYear, size: 100, }, { accessorKey: "division", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="구분" />, - cell: ({ row }) => getDivisionBadge(row.getValue("division")), + header: createHeaderRenderer("구분"), + cell: renderDivision, size: 80, }, { accessorKey: "status", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="상태" />, - cell: ({ row }) => { - const status = row.getValue<string>("status"); - return ( - <Badge variant={getStatusBadgeVariant(status)}> - {getStatusText(status)} - </Badge> - ); - }, + header: createHeaderRenderer("상태"), + cell: renderStatus, size: 100, }, { accessorKey: "consensusStatus", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="의견 일치" />, - cell: ({ row }) => getConsensusBadge(row.getValue("consensusStatus")), + header: createHeaderRenderer("의견 일치"), + cell: renderConsensusStatus, size: 100, }, - // ✅ 벤더 정보 그룹 + // 벤더 정보 { id: "vendorInfo", header: "벤더 정보", columns: [ { accessorKey: "vendorCode", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="벤더 코드" />, - cell: ({ row }) => ( - <span className="font-mono text-sm">{row.getValue("vendorCode")}</span> - ), + header: createHeaderRenderer("벤더 코드"), + cell: renderVendorCode, size: 120, }, { accessorKey: "vendorName", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="벤더명" />, - cell: ({ row }) => ( - <div className="truncate max-w-[200px]" title={row.getValue<string>("vendorName")!}> - {row.getValue("vendorName") as string} - </div> - ), + header: createHeaderRenderer("벤더명"), + cell: renderVendorName, size: 200, }, { accessorKey: "domesticForeign", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="내외자" />, - cell: ({ row }) => getDomesticForeignBadge(row.getValue("domesticForeign")), + header: createHeaderRenderer("내외자"), + cell: renderDomesticForeign, size: 80, }, { accessorKey: "materialType", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="자재구분" />, - cell: ({ row }) => getMaterialTypeBadge(row.getValue("materialType")), + header: createHeaderRenderer("자재구분"), + cell: renderMaterialType, size: 120, }, ] }, - // ✅ 발주 담당자 + // 발주 담당자 { id: "orderReviewer", header: "발주 담당자", columns: [ { accessorKey: "orderReviewerName", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="담당자명" />, - cell: ({ row }) => { - const reviewerName = row.getValue<string>("orderReviewerName"); - return reviewerName ? ( - <div className="truncate max-w-[120px]" title={reviewerName}> - {reviewerName} - </div> - ) : ( - <span className="text-muted-foreground">-</span> - ); - }, + header: createHeaderRenderer("담당자명"), + cell: renderReviewerName("orderReviewerName"), size: 120, }, { accessorKey: "orderIsApproved", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="평가 대상" />, - cell: ({ row }) => { - const isApproved = row.getValue<boolean>("orderIsApproved"); - return getEvaluationTargetBadge(isApproved); - }, + header: createHeaderRenderer("평가 대상"), + cell: renderIsApproved("orderIsApproved"), size: 120, }, ] }, - // ✅ 조달 담당자 + // 조달 담당자 { id: "procurementReviewer", header: "조달 담당자", columns: [ { accessorKey: "procurementReviewerName", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="담당자명" />, - cell: ({ row }) => { - const reviewerName = row.getValue<string>("procurementReviewerName"); - return reviewerName ? ( - <div className="truncate max-w-[120px]" title={reviewerName}> - {reviewerName} - </div> - ) : ( - <span className="text-muted-foreground">-</span> - ); - }, + header: createHeaderRenderer("담당자명"), + cell: renderReviewerName("procurementReviewerName"), size: 120, }, { accessorKey: "procurementIsApproved", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="평가 대상" />, - cell: ({ row }) => { - const isApproved = row.getValue<boolean>("procurementIsApproved"); - return getEvaluationTargetBadge(isApproved); - }, + header: createHeaderRenderer("평가 대상"), + cell: renderIsApproved("procurementIsApproved"), size: 120, }, ] }, - // ✅ 품질 담당자 + // 품질 담당자 { id: "qualityReviewer", header: "품질 담당자", columns: [ { accessorKey: "qualityReviewerName", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="담당자명" />, - cell: ({ row }) => { - const reviewerName = row.getValue<string>("qualityReviewerName"); - return reviewerName ? ( - <div className="truncate max-w-[120px]" title={reviewerName}> - {reviewerName} - </div> - ) : ( - <span className="text-muted-foreground">-</span> - ); - }, + header: createHeaderRenderer("담당자명"), + cell: renderReviewerName("qualityReviewerName"), size: 120, }, { accessorKey: "qualityIsApproved", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="평가 대상" />, - cell: ({ row }) => { - const isApproved = row.getValue<boolean>("qualityIsApproved"); - return getEvaluationTargetBadge(isApproved); - }, + header: createHeaderRenderer("평가 대상"), + cell: renderIsApproved("qualityIsApproved"), size: 120, }, ] }, - // ✅ 설계 담당자 + // 설계 담당자 { id: "designReviewer", header: "설계 담당자", columns: [ { accessorKey: "designReviewerName", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="담당자명" />, - cell: ({ row }) => { - const reviewerName = row.getValue<string>("designReviewerName"); - return reviewerName ? ( - <div className="truncate max-w-[120px]" title={reviewerName}> - {reviewerName} - </div> - ) : ( - <span className="text-muted-foreground">-</span> - ); - }, + header: createHeaderRenderer("담당자명"), + cell: renderReviewerName("designReviewerName"), size: 120, }, { accessorKey: "designIsApproved", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="평가 대상" />, - cell: ({ row }) => { - const isApproved = row.getValue<boolean>("designIsApproved"); - return getEvaluationTargetBadge(isApproved); - }, + header: createHeaderRenderer("평가 대상"), + cell: renderIsApproved("designIsApproved"), size: 120, }, ] }, - // ✅ CS 담당자 + // CS 담당자 { id: "csReviewer", header: "CS 담당자", columns: [ { accessorKey: "csReviewerName", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="담당자명" />, - cell: ({ row }) => { - const reviewerName = row.getValue<string>("csReviewerName"); - return reviewerName ? ( - <div className="truncate max-w-[120px]" title={reviewerName}> - {reviewerName} - </div> - ) : ( - <span className="text-muted-foreground">-</span> - ); - }, + header: createHeaderRenderer("담당자명"), + cell: renderReviewerName("csReviewerName"), size: 120, }, { accessorKey: "csIsApproved", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="평가 대상" />, - cell: ({ row }) => { - const isApproved = row.getValue<boolean>("csIsApproved"); - return getEvaluationTargetBadge(isApproved); - }, + header: createHeaderRenderer("평가 대상"), + cell: renderIsApproved("csIsApproved"), size: 120, }, ] }, - // ✅ 의견 및 결과 + // 의견 및 결과 { accessorKey: "adminComment", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="관리자 의견" />, - cell: ({ row }) => { - const comment = row.getValue<string>("adminComment"); - return comment ? ( - <div className="truncate max-w-[150px]" title={comment}> - {comment} - </div> - ) : ( - <span className="text-muted-foreground">-</span> - ); - }, + header: createHeaderRenderer("관리자 의견"), + cell: renderComment("max-w-[150px]"), size: 150, }, { accessorKey: "consolidatedComment", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="종합 의견" />, - cell: ({ row }) => { - const comment = row.getValue<string>("consolidatedComment"); - return comment ? ( - <div className="truncate max-w-[150px]" title={comment}> - {comment} - </div> - ) : ( - <span className="text-muted-foreground">-</span> - ); - }, + header: createHeaderRenderer("종합 의견"), + cell: renderComment("max-w-[150px]"), size: 150, }, { accessorKey: "confirmedAt", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="확정일" />, - cell: ({ row }) => { - const confirmedAt = row.getValue<Date>("confirmedAt"); - return <span className="text-sm">{ confirmedAt ? formatDate(confirmedAt, "KR") :'-'}</span>; - }, + header: createHeaderRenderer("확정일"), + cell: renderConfirmedAt, size: 100, }, { accessorKey: "createdAt", - header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="생성일" />, - cell: ({ row }) => { - const createdAt = row.getValue<Date>("createdAt"); - return <span className="text-sm">{formatDate(createdAt, "KR")}</span>; - }, + header: createHeaderRenderer("생성일"), + cell: renderCreatedAt, size: 100, }, - // ✅ Actions - 가장 안전하게 처리 + // Actions { id: "actions", enableHiding: false, size: 40, minSize: 40, - cell: ({ row }) => { - // ✅ 함수를 직접 정의해서 매번 새로 생성되지 않도록 처리 - const handleEdit = () => { - setRowAction({ row, type: "update" }); - }; - - return ( - <div className="flex items-center gap-1"> - <Button - variant="ghost" - size="icon" - className="size-8" - onClick={handleEdit} - aria-label="수정" - title="수정" - > - <Pencil className="size-4" /> - </Button> - </div> - ); - }, + cell: renderActionsCell, }, ]; +} + +// ✅ WeakMap 캐시로 setRowAction별로 컬럼 캐싱 +const columnsCache = new WeakMap<GetColumnsProps['setRowAction'], ColumnDef<EvaluationTargetWithDepartments>[]>(); + +export function getEvaluationTargetsColumns({ setRowAction }: GetColumnsProps): ColumnDef<EvaluationTargetWithDepartments>[] { + // 캐시 확인 + if (columnsCache.has(setRowAction)) { + console.log('✅ 캐시된 컬럼 사용'); + return columnsCache.get(setRowAction)!; + } + + console.log('🏗️ 새로운 컬럼 생성'); + const columns = createStaticColumns(setRowAction); + columnsCache.set(setRowAction, columns); + return columns; }
\ No newline at end of file |
