From 90f79a7a691943a496f67f01c1e493256070e4de Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Mon, 7 Jul 2025 01:44:45 +0000
Subject: (대표님) 변경사항 20250707 10시 43분 - unstaged 변경사항 추가
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../table/evaluation-targets-columns.tsx           | 405 ++++++++++-----------
 1 file changed, 191 insertions(+), 214 deletions(-)

(limited to 'lib/evaluation-target-list/table/evaluation-targets-columns.tsx')

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
-- 
cgit v1.2.3

