summaryrefslogtreecommitdiff
path: root/lib/evaluation-target-list/table/evaluation-targets-columns.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/evaluation-target-list/table/evaluation-targets-columns.tsx')
-rw-r--r--lib/evaluation-target-list/table/evaluation-targets-columns.tsx405
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