"use client" import * as React from "react" import { type DataTableRowAction } from "@/types/table" import { type ColumnDef } from "@tanstack/react-table" import { Paperclip } from "lucide-react" import { toast } from "sonner" import { getErrorMessage } from "@/lib/handle-error" import { formatDate, formatDateTime } from "@/lib/utils" import { downloadFile } from "@/lib/file-download" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Checkbox } from "@/components/ui/checkbox" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" import { basicContractColumnsConfig, basicContractVendorColumnsConfig } from "@/config/basicContractColumnsConfig" import { BasicContractView } from "@/db/schema" interface GetColumnsProps { setRowAction: React.Dispatch | null>> locale?: string t: (key: string) => string // 번역 함수 } // 기본 번역값들 (fallback) const fallbackTranslations = { ko: { download: "다운로드", selectAll: "전체 선택", selectRow: "행 선택", fileInfoMissing: "파일 정보가 없습니다.", fileDownloadError: "파일 다운로드 중 오류가 발생했습니다.", statusValues: { PENDING: "서명대기", COMPLETED: "서명완료" } }, en: { download: "Download", selectAll: "Select all", selectRow: "Select row", fileInfoMissing: "File information is missing.", fileDownloadError: "An error occurred while downloading the file.", statusValues: { PENDING: "Pending", COMPLETED: "Completed" } } }; // 안전한 번역 함수 const safeTranslate = (t: (key: string) => string, key: string, locale: string = 'ko', fallback?: string): string => { try { const translated = t(key); // 번역 키가 그대로 반환되는 경우 (번역 실패) fallback 사용 if (translated === key && fallback) { return fallback; } return translated || fallback || key; } catch (error) { console.warn(`Translation failed for key: ${key}`, error); return fallback || key; } }; /** * 파일 다운로드 함수 */ const handleFileDownload = async ( filePath: string | null, fileName: string | null, t: (key: string) => string, locale: string = 'ko' ) => { const fallback = fallbackTranslations[locale as keyof typeof fallbackTranslations] || fallbackTranslations.ko; if (!filePath || !fileName) { const message = safeTranslate(t, "basicContracts.fileInfoMissing", locale, fallback.fileInfoMissing); toast.error(message); return; } try { // /api/files/ 엔드포인트를 통한 안전한 다운로드 const apiFilePath = `/api/files/${filePath.startsWith('/') ? filePath.substring(1) : filePath}`; const result = await downloadFile(apiFilePath, fileName, { action: 'download', showToast: true }); if (!result.success) { console.error("파일 다운로드 실패:", result.error); } } catch (error) { console.error("파일 다운로드 오류:", error); const message = safeTranslate(t, "basicContracts.fileDownloadError", locale, fallback.fileDownloadError); toast.error(message); } }; /** * tanstack table 컬럼 정의 (중첩 헤더 버전) */ export function getColumns({ setRowAction, locale = 'ko', t }: GetColumnsProps): ColumnDef[] { const fallback = fallbackTranslations[locale as keyof typeof fallbackTranslations] || fallbackTranslations.ko; // ---------------------------------------------------------------- // 1) select 컬럼 (체크박스) // ---------------------------------------------------------------- const selectColumn: ColumnDef = { id: "select", header: ({ table }) => ( table.toggleAllPageRowsSelected(!!value)} aria-label={safeTranslate(t, "basicContracts.selectAll", locale, fallback.selectAll)} className="translate-y-0.5" /> ), cell: ({ row }) => ( row.toggleSelected(!!value)} aria-label={safeTranslate(t, "basicContracts.selectRow", locale, fallback.selectRow)} className="translate-y-0.5" /> ), maxSize: 30, enableSorting: false, enableHiding: false, } // ---------------------------------------------------------------- // 2) 파일 다운로드 컬럼 (아이콘) // ---------------------------------------------------------------- const downloadColumn: ColumnDef = { id: "download", header: "", cell: ({ row }) => { const contract = row.original; // PENDING 상태일 때는 원본 PDF 파일 (signedFilePath), COMPLETED일 때는 서명된 파일 (signedFilePath) const filePath = contract.signedFilePath; const fileName = contract.signedFileName; const downloadText = safeTranslate(t, "basicContracts.download", locale, fallback.download); return ( ); }, maxSize: 30, enableSorting: false, } // ---------------------------------------------------------------- // 4) 일반 컬럼들을 "그룹"별로 묶어 중첩 columns 생성 // ---------------------------------------------------------------- // 4-1) groupMap: { [groupName]: ColumnDef[] } const groupMap: Record[]> = {} basicContractVendorColumnsConfig.forEach((cfg) => { // 만약 group가 없으면 "_noGroup" 처리 const groupName = cfg.group || "_noGroup" if (!groupMap[groupName]) { groupMap[groupName] = [] } // child column 정의 const childCol: ColumnDef = { accessorKey: cfg.id, enableResizing: true, header: ({ column }) => ( ), meta: { excelHeader: cfg.excelHeader, group: cfg.group, type: cfg.type, }, cell: ({ row, cell }) => { // 날짜 형식 처리 - 로케일 적용 if (cfg.id === "createdAt" || cfg.id === "updatedAt" || cfg.id === "completedAt") { const dateVal = cell.getValue() as Date return formatDateTime(dateVal, locale) } // Status 컬럼에 Badge 적용 - 다국어 적용 if (cfg.id === "status") { const status = row.getValue(cfg.id) as string const isPending = status === "PENDING" const statusText = safeTranslate( t, `basicContracts.statusValues.${status}`, locale, fallback.statusValues[status as keyof typeof fallback.statusValues] || status ); return ( {statusText} ) } // 나머지 컬럼은 그대로 값 표시 return row.getValue(cfg.id) ?? "" }, minSize: 80, } groupMap[groupName].push(childCol) }) const contractTypeColumn: ColumnDef = { id: "contractType", accessorFn: (row) => { // 계약 유형 판별 로직 if (row.generalContractId) return "contract"; if (row.rfqCompanyId) return "quotation"; if (row.biddingCompanyId) return "bidding"; return "general"; }, header: ({ column }) => ( ), cell: ({ getValue }) => { const type = getValue() as string; // 타입별 표시 텍스트와 스타일 정의 const typeConfig = { general: { label: locale === 'ko' ? '일반' : 'General', variant: 'outline' as const }, quotation: { label: locale === 'ko' ? '견적' : 'Quotation', variant: 'secondary' as const }, contract: { label: locale === 'ko' ? '계약' : 'Contract', variant: 'default' as const }, bidding: { label: locale === 'ko' ? '입찰' : 'Bidding', variant: 'destructive' as const } }; const config = typeConfig[type as keyof typeof typeConfig] || typeConfig.general; return ( {config.label} ); }, enableSorting: true, enableHiding: true, minSize: 80, }; // ---------------------------------------------------------------- // 4-2) groupMap에서 실제 상위 컬럼(그룹)을 만들기 // ---------------------------------------------------------------- const nestedColumns: ColumnDef[] = [] // 순서를 고정하고 싶다면 group 순서를 미리 정의하거나 sort해야 함 // 여기서는 그냥 Object.entries 순서 Object.entries(groupMap).forEach(([groupName, colDefs]) => { if (groupName === "_noGroup") { // 그룹 없음 → 그냥 최상위 레벨 컬럼 nestedColumns.push(...colDefs) } else { // 상위 컬럼 - 그룹명 다국어 적용 const translatedGroupName = t(`basicContracts.groups.${groupName}`) || groupName; nestedColumns.push({ id: groupName, header: translatedGroupName, columns: colDefs, }) } }) // ---------------------------------------------------------------- // 5) 최종 컬럼 배열: select, download, nestedColumns, actions // ---------------------------------------------------------------- return [ selectColumn, downloadColumn, // 다운로드 컬럼 추가 contractTypeColumn, ...nestedColumns, ] }