"use client" import * as React from "react" import { type DataTableRowAction } from "@/types/table" import { type ColumnDef } from "@tanstack/react-table" // ColumnDef 타입 확장 type ExtendedColumnDef = ColumnDef & { excelHeader?: string; } import { Ellipsis, Eye, FileEdit, Trash2, Building2, FileText, Edit } from "lucide-react" import { formatDate } from "@/lib/utils" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Checkbox } from "@/components/ui/checkbox" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header"; import { useRouter, useParams } from "next/navigation" import { PQDeleteDialog } from "@/components/pq-input/pq-delete-dialog" // PQ 제출 타입 정의 export interface PQSubmission { // PQ 제출 정보 id: number pqNumber: string type: string status: string requesterName: string | null // 요청자 이름 createdAt: Date updatedAt: Date submittedAt: Date | null approvedAt: Date | null rejectedAt: Date | null rejectReason: string | null // 협력업체 정보 vendorId: number vendorName: string vendorCode: string taxId: string vendorStatus: string email: string // 프로젝트 정보 projectId: number | null projectName: string | null projectCode: string | null // 답변 정보 answerCount: number attachmentCount: number // PQ 상태 pqStatus: string pqTypeLabel: string // PQ 대상품목 pqItems: string | null | Array<{itemCode: string, itemName: string}> // 방문실사 요청 정보 siteVisitRequestId: number | null // 방문실사 요청 ID // 실사 정보 investigation: { id: number investigationStatus: string requesterName: string | null // 실사 요청자 이름 qmManagerId: number | null qmManagerName: string | null // QM 담당자 이름 qmManagerEmail: string | null // QM 담당자 이메일 investigationAddress: string | null investigationMethod: string | null hasSupplementRequested: boolean scheduledStartAt: Date | null scheduledEndAt: Date | null requestedAt: Date | null confirmedAt: Date | null completedAt: Date | null forecastedAt: Date | null evaluationScore: number | null evaluationResult: "APPROVED" | "SUPPLEMENT" | "SUPPLEMENT_REINSPECT" | "SUPPLEMENT_DOCUMENT" | "REJECTED" | "RESULT_SENT" | null investigationNotes: string | null } | null // 통합 상태를 위한 새 필드 combinedStatus: { status: string label: string variant: "default" | "outline" | "secondary" | "destructive" | "success" } } type NextRouter = ReturnType; interface GetColumnsProps { setRowAction: React.Dispatch | null>>; router: NextRouter; } /** * tanstack table 컬럼 정의 */ export function getColumns({ setRowAction, router }: GetColumnsProps): ExtendedColumnDef[] { // ---------------------------------------------------------------- // 1) select 컬럼 (체크박스) // ---------------------------------------------------------------- const selectColumn: ExtendedColumnDef = { id: "select", header: ({ table }) => { const selectedRows = table.getSelectedRowModel().rows; const isAllSelected = table.getIsAllPageRowsSelected(); const isSomeSelected = table.getIsSomePageRowsSelected(); return ( { if (value) { // 전체 선택: 첫 번째 행만 선택 table.toggleAllRowsSelected(false); if (table.getRowModel().rows.length > 0) { table.getRowModel().rows[0].toggleSelected(true); } } else { // 전체 해제 table.toggleAllRowsSelected(false); } }} aria-label="Select all" className="translate-y-0.5" /> ); }, cell: ({ row, table }) => { const selectedRows = table.getSelectedRowModel().rows; const isCurrentlySelected = row.getIsSelected(); return ( { if (value) { // 체크하려는 경우: 이미 선택된 행이 있으면 모두 해제하고 현재 행만 선택 table.toggleAllRowsSelected(false); row.toggleSelected(true); } else { // 체크 해제하는 경우 row.toggleSelected(false); } }} aria-label="Select row" className="translate-y-0.5" /> ); }, size: 40, enableSorting: false, enableHiding: false, } // ---------------------------------------------------------------- // 2) 일반 컬럼들 // -------------------------- // -------------------------------------- const pqNoColumn: ExtendedColumnDef = { accessorKey: "pqNumber", header: ({ column }) => ( ), cell: ({ row }) => (
{row.getValue("pqNumber")}
), meta: { excelHeader: "PQ No.", }, } // 협력업체 컬럼 const vendorColumn: ExtendedColumnDef = { accessorKey: "vendorName", header: ({ column }) => ( ), cell: ({ row }) => (
{row.getValue("vendorName")} {/* {row.original.vendorCode ? row.original.vendorCode : "-"}/{row.original.taxId} */}
), enableSorting: true, enableHiding: true, meta: { excelHeader: "협력업체", }, } // PQ 유형 컬럼 const typeColumn: ExtendedColumnDef = { accessorKey: "type", header: ({ column }) => ( ), cell: ({ row }) => { const { type, pqTypeLabel } = row.original; let label = pqTypeLabel; if (type === "NON_INSPECTION") { label = "미실사 PQ"; } return (
{label}
); }, filterFn: (row, id, value) => { return value.includes(row.getValue(id)); }, enableSorting: true, enableHiding: true, meta: { excelHeader: "PQ 유형", }, } // 프로젝트 컬럼 const projectColumn: ExtendedColumnDef = { accessorKey: "projectName", header: ({ column }) => ( ), cell: ({ row }) => { const projectName = row.original.projectName const projectCode = row.original.projectCode if (!projectName) { return - } return (
{projectName} {projectCode && ( {projectCode} )}
) }, enableSorting: true, enableHiding: true, meta: { excelHeader: "프로젝트", }, } // 상태 컬럼 const statusColumn: ExtendedColumnDef = { accessorKey: "combinedStatus", header: ({ column }) => ( ), cell: ({ row }) => { const combinedStatus = getCombinedStatus(row.original); return {combinedStatus.label}; }, filterFn: (row, id, value) => { const combinedStatus = getCombinedStatus(row.original); return value.includes(combinedStatus.status); }, enableSorting: false, enableHiding: true, meta: { excelHeader: "진행현황", }, }; // PQ 상태와 실사 상태를 결합하는 헬퍼 함수 function getCombinedStatus(submission: PQSubmission) { // PQ가 QM 승인되지 않은 경우, PQ 상태를 우선 표시 if (submission.status !== "QM_APPROVED") { switch (submission.status) { case "REQUESTED": return { status: "PQ_REQUESTED", label: "PQ 요청됨", variant: "outline" as const }; case "IN_PROGRESS": return { status: "PQ_IN_PROGRESS", label: "PQ 진행 중", variant: "secondary" as const }; case "SUBMITTED": return { status: "PQ_SUBMITTED", label: "PQ 제출됨", variant: "default" as const }; case "APPROVED": return { status: "PQ_APPROVED", label: "PQ 승인됨", variant: "success" as const }; case "REJECTED": return { status: "PQ_REJECTED", label: "PQ 거부됨", variant: "destructive" as const }; case "QM_REVIEWING": return { status: "PQ_QM_REVIEWING", label: "QM 검토 중", variant: "secondary" as const }; case "QM_REJECTED": return { status: "PQ_QM_REJECTED", label: "QM 거부됨", variant: "destructive" as const }; default: return { status: submission.status, label: submission.status, variant: "outline" as const }; } } // PQ가 QM 승인되었지만 실사가 없는 경우 if (!submission.investigation) { return { status: "PQ_QM_APPROVED", label: "PQ 승인됨", variant: "success" as const }; } // PQ가 승인되고 실사가 있는 경우 switch (submission.investigation.investigationStatus) { case "PLANNED": return { status: "INVESTIGATION_PLANNED", label: "실사 계획됨", variant: "outline" as const }; case "QM_REVIEW_CONFIRMED": return { status: "INVESTIGATION_QM_REVIEW_CONFIRMED", label: "QM 검토 완료", variant: "outline" as const }; case "IN_PROGRESS": return { status: "INVESTIGATION_IN_PROGRESS", label: "실사 진행 중", variant: "secondary" as const }; case "COMPLETED": // 실사 완료 후 평가 결과에 따라 다른 상태 표시 if (submission.investigation.evaluationResult) { switch (submission.investigation.evaluationResult) { case "APPROVED": return { status: "INVESTIGATION_APPROVED", label: "실사 승인", variant: "success" as const }; case "SUPPLEMENT": return { status: "INVESTIGATION_SUPPLEMENT", label: "실사 보완필요", variant: "secondary" as const }; case "SUPPLEMENT_REINSPECT": return { status: "INVESTIGATION_SUPPLEMENT_REINSPECT", label: "실사 보완-재실사", variant: "secondary" as const }; case "SUPPLEMENT_DOCUMENT": return { status: "INVESTIGATION_SUPPLEMENT_DOCUMENT", label: "실사 보완-서류제출", variant: "secondary" as const }; case "REJECTED": return { status: "INVESTIGATION_REJECTED", label: "실사 불가", variant: "destructive" as const }; default: return { status: "INVESTIGATION_COMPLETED", label: "실사 완료", variant: "default" as const }; } } return { status: "INVESTIGATION_COMPLETED", label: "실사 완료", variant: "default" as const }; case "CANCELED": return { status: "INVESTIGATION_CANCELED", label: "실사 취소됨", variant: "destructive" as const }; case "SUPPLEMENT_REQUIRED": return { status: "INVESTIGATION_SUPPLEMENT_REQUIRED", label: "실사 보완 요구됨", variant: "secondary" as const }; case "RESULT_SENT": // 보완을 통해 최종 합격/불합격한 경우 if (submission.investigation.hasSupplementRequested) { return { status: "INVESTIGATION_RESULT_SENT_SUPPLEMENT", label: "실사 결과 발송(보완)", variant: "success" as const }; } return { status: "INVESTIGATION_RESULT_SENT", label: "실사 결과 발송", variant: "success" as const }; default: return { status: `INVESTIGATION_${submission.investigation.investigationStatus}`, label: `실사 ${submission.investigation.investigationStatus}`, variant: "outline" as const }; } } // 평가유형 컬럼 (QM실사방법으로 교체됨) // const evaluationTypeColumn: ColumnDef = { // accessorKey: "evaluationType", // header: ({ column }) => ( // // ), // cell: ({ row }) => { // const investigation = row.original.investigation; // if (!investigation || !investigation.evaluationType) { // return -; // } // switch (investigation.evaluationType) { // case "PURCHASE_SELF_EVAL": // return 구매자체평가; // case "DOCUMENT_EVAL": // return 서류평가; // case "PRODUCT_INSPECTION": // return 제품검사평가; // case "SITE_VISIT_EVAL": // return 방문실사평가; // default: // return {investigation.evaluationType}; // } // }, // filterFn: (row, id, value) => { // const investigation = row.original.investigation; // if (!investigation || !investigation.evaluationType) return value.includes("null"); // return value.includes(investigation.evaluationType); // }, // }; const evaluationResultColumn: ExtendedColumnDef = { accessorKey: "evaluationResult", header: ({ column }) => ( ), cell: ({ row }) => { const investigation = row.original.investigation; if (!investigation || !investigation.evaluationResult) { return -; } switch (investigation.evaluationResult) { case "APPROVED": return 승인; case "SUPPLEMENT": return 보완; case "SUPPLEMENT_REINSPECT": return 보완-재실사; case "SUPPLEMENT_DOCUMENT": return 보완-서류제출; case "REJECTED": return 불가; default: return {investigation.evaluationResult}; } }, filterFn: (row, id, value) => { const investigation = row.original.investigation; if (!investigation || !investigation.evaluationResult) return value.includes("null"); return value.includes(investigation.evaluationResult); }, enableSorting: true, enableHiding: true, meta: { excelHeader: "평가 결과", }, }; // 답변 수 컬럼 const answerCountColumn: ExtendedColumnDef = { accessorKey: "answerCount", header: ({ column }) => ( ), cell: ({ row }) => { return (
{row.original.answerCount}
) }, meta: { excelHeader: "답변 수", }, } const investigationAddressColumn: ExtendedColumnDef = { accessorKey: "investigationAddress", header: ({ column }) => ( ), cell: ({ row }) => { const investigation = row.original.investigation; if (!investigation || !investigation.investigationAddress) { return -; } return (
{investigation.investigationAddress}
) }, meta: { excelHeader: "실사 주소", }, } const investigationRequestedAtColumn: ExtendedColumnDef = { accessorKey: "investigationRequestedAt", header: ({ column }) => ( ), cell: ({ row }) => { const investigation = row.original.investigation; if (!investigation || !investigation.requestedAt) { return -; } const dateVal = investigation.requestedAt return (
{dateVal ? formatDate(dateVal, 'KR') : "-"}
) }, meta: { excelHeader: "실사 의뢰일", }, } const investigationNotesColumn: ExtendedColumnDef = { accessorKey: "investigationNotes", header: ({ column }) => ( ), cell: ({ row }) => { const investigation = row.original.investigation; if (!investigation || !investigation.investigationNotes) { return -; } return (
{investigation.investigationNotes}
) }, meta: { excelHeader: "QM 의견", }, } const investigationMethodColumn: ExtendedColumnDef = { accessorKey: "investigationMethod", header: ({ column }) => ( ), cell: ({ row }) => { const investigation = row.original.investigation; if (!investigation || !investigation.investigationMethod) { return -; } switch (investigation.investigationMethod) { case "PURCHASE_SELF_EVAL": return 구매자체평가; case "DOCUMENT_EVAL": return 서류평가; case "PRODUCT_INSPECTION": return 제품검사평가; case "SITE_VISIT_EVAL": return 방문실사평가; default: return {investigation.investigationMethod}; } }, meta: { excelHeader: "QM실사방법", }, } // 실사품목 컬럼 const pqItemsColumn: ExtendedColumnDef = { accessorKey: "pqItems", header: ({ column }) => ( ), cell: ({ row }) => { const pqItems = row.original.pqItems; if (!pqItems) { return -; } // JSON 파싱하여 첫 번째 아이템 표시 const items = typeof pqItems === 'string' ? JSON.parse(pqItems) : pqItems; if (Array.isArray(items) && items.length > 0) { const firstItem = items[0]; return (
{firstItem.itemCode} - {firstItem.itemName} {items.length > 1 && ( 외 {items.length - 1}건 )}
); } }, meta: { excelHeader: "실사품목", }, } const investigationForecastedAtColumn: ExtendedColumnDef = { accessorKey: "investigationForecastedAt", header: ({ column }) => ( ), cell: ({ row }) => { const investigation = row.original.investigation; if (!investigation || !investigation.forecastedAt) { return -; } const dateVal = investigation.forecastedAt return (
{dateVal ? formatDate(dateVal, 'KR') : "-"}
) }, meta: { excelHeader: "실사 수행 예정일", }, } const investigationConfirmedAtColumn: ExtendedColumnDef = { accessorKey: "investigationConfirmedAt", header: ({ column }) => ( ), cell: ({ row }) => { const investigation = row.original.investigation; if (!investigation || !investigation.confirmedAt) { return -; } const dateVal = investigation.confirmedAt return (
{dateVal ? formatDate(dateVal, 'KR') : "-"}
) }, meta: { excelHeader: "실사 계획 확정일", }, } const investigationCompletedAtColumn: ExtendedColumnDef = { accessorKey: "investigationCompletedAt", header: ({ column }) => ( ), cell: ({ row }) => { const investigation = row.original.investigation; if (!investigation || !investigation.completedAt) { return -; } const dateVal = investigation.completedAt return (
{dateVal ? formatDate(dateVal, 'KR') : "-"}
) }, meta: { excelHeader: "실제 실사일", }, } // 제출일 컬럼 const createdAtColumn: ExtendedColumnDef = { accessorKey: "createdAt", header: ({ column }) => ( ), cell: ({ row }) => { const dateVal = row.original.createdAt as Date return formatDate(dateVal, 'KR') }, meta: { excelHeader: "PQ 전송일", }, } // 제출일 컬럼 const submittedAtColumn: ExtendedColumnDef = { accessorKey: "submittedAt", header: ({ column }) => ( ), cell: ({ row }) => { const dateVal = row.original.submittedAt as Date return dateVal ? formatDate(dateVal, 'KR') : "-" }, meta: { excelHeader: "PQ 회신일", }, } // 승인/거부일 컬럼 const approvalDateColumn: ExtendedColumnDef = { accessorKey: "approvedAt", header: ({ column }) => ( ), cell: ({ row }) => { if (row.original.approvedAt) { return {formatDate(row.original.approvedAt)} } if (row.original.rejectedAt) { return {formatDate(row.original.rejectedAt)} } return "-" }, meta: { excelHeader: "PQ 승인/거부일", }, } // ---------------------------------------------------------------- // 3) actions 컬럼 (Dropdown 메뉴) // ---------------------------------------------------------------- const actionsColumn: ExtendedColumnDef = { id: "actions", header: ({ column }) => ( ), enableHiding: false, meta: { excelHeader: "보기", }, cell: function Cell({ row }) { const pq = row.original const isSubmitted = pq.status === "SUBMITTED" const reviewUrl = `/evcp/pq_new/${pq.vendorId}/${pq.id}` return ( { router.push(reviewUrl); }} > {isSubmitted ? ( <> 검토 ) : ( <> PQ 현황 )} {/* 방문실사 버튼 - PQ가 승인됨 상태이고 제품검사평가 또는 방문실사평가인 경우에만 표시 */} {pq.status === "APPROVED" && pq.investigation && (pq.investigation.investigationMethod === "PRODUCT_INSPECTION" || pq.investigation.investigationMethod === "SITE_VISIT_EVAL") && ( <> { e.preventDefault(); // 방문실사 다이얼로그 열기 로직 setRowAction({ type: "site-visit", row: row.original }); }} > 실사 정보 전달 및 요청 { e.preventDefault(); // 협력업체 정보 조회 다이얼로그 열기 로직 setRowAction({ type: "vendor-info-view", row: row.original }); }} > 실사 실시 확정 정보 )} {/* 실사 정보 수정 버튼 - 구매자체평가인 경우에만 표시 */} {pq.investigation && pq.investigation.investigationMethod === "PURCHASE_SELF_EVAL" && ( { e.preventDefault(); // 실사 정보 수정 다이얼로그 열기 로직 setRowAction({ type: "update", row: row.original }); }} > 구매 자체 평가 )} {/* 삭제 메뉴 - REQUESTED 상태일 때만 표시 */} {pq.status === "REQUESTED" && ( { e.preventDefault(); }} className="text-destructive focus:text-destructive" > 삭제 )} ) }, size: 40, } // 요청자 컬럼 추가 const requesterColumn: ExtendedColumnDef = { accessorKey: "requesterName", header: ({ column }) => ( ), cell: ({ row }) => { // PQ 요청자와 실사 요청자를 모두 표시 const pqRequesterName = row.original.requesterName; const investigationRequesterName = row.original.investigation?.requesterName; // 상태에 따라 적절한 요청자 표시 const status = getCombinedStatus(row.original).status; if (status.startsWith('INVESTIGATION_') && investigationRequesterName) { return {investigationRequesterName}; } return pqRequesterName ? {pqRequesterName} : -; }, meta: { excelHeader: "PQ/실사 요청자", }, }; const qmManagerColumn: ExtendedColumnDef = { accessorKey: "qmManager", header: ({ column }) => ( ), cell: ({ row }) => { const investigation = row.original.investigation; if (!investigation || !investigation.qmManagerName) { return -; } return (
{investigation.qmManagerName} {investigation.qmManagerEmail && ( {investigation.qmManagerEmail} )}
); }, meta: { excelHeader: "QM 담당자", }, }; // ---------------------------------------------------------------- // 4) 최종 컬럼 배열 // ---------------------------------------------------------------- return [ selectColumn, statusColumn, // 통합된 진행현황 컬럼 pqNoColumn, vendorColumn, investigationAddressColumn, typeColumn, projectColumn, pqItemsColumn, // 실사품목 컬럼 createdAtColumn, submittedAtColumn, approvalDateColumn, answerCountColumn, investigationMethodColumn, investigationForecastedAtColumn, investigationRequestedAtColumn, investigationConfirmedAtColumn, investigationCompletedAtColumn, evaluationResultColumn, // 평가 결과 컬럼 requesterColumn, qmManagerColumn, investigationNotesColumn, actionsColumn, ]; }