diff options
Diffstat (limited to 'lib/vendor-document-list')
6 files changed, 1011 insertions, 1 deletions
diff --git a/lib/vendor-document-list/dolce-upload-service.ts b/lib/vendor-document-list/dolce-upload-service.ts index 84ae4525..8835c0b0 100644 --- a/lib/vendor-document-list/dolce-upload-service.ts +++ b/lib/vendor-document-list/dolce-upload-service.ts @@ -718,7 +718,8 @@ class DOLCEUploadService { return { Mode: mode, - Status: revision.revisionStatus || "Standby", + // Status: revision.revisionStatus || "Standby", + Status: "Standby", RegisterId: revision.registerId || 0, // registerId가 없으면 0 (ADD 모드) ProjectNo: contractInfo.projectCode, Discipline: revision.discipline || "DL", diff --git a/lib/vendor-document-list/enhanced-document-service.ts b/lib/vendor-document-list/enhanced-document-service.ts index f2d9c26f..7464b13f 100644 --- a/lib/vendor-document-list/enhanced-document-service.ts +++ b/lib/vendor-document-list/enhanced-document-service.ts @@ -1173,6 +1173,139 @@ export async function getDocumentDetails(documentId: number) { } } + export async function getUserVendorDocumentsAll( + userId: number, + input: GetVendorShipDcoumentsSchema + ) { + try { + + const session = await getServerSession(authOptions) + if (!session?.user?.id) { + throw new Error("인증이 필요합니다.") + } + + + + const offset = (input.page - 1) * input.perPage + + + + // 3. 고급 필터 처리 + const advancedWhere = filterColumns({ + table: simplifiedDocumentsView, + filters: input.filters || [], + joinOperator: input.joinOperator || "and", + }) + + // 4. 전역 검색 처리 + let globalWhere + if (input.search) { + const searchTerm = `%${input.search}%` + globalWhere = or( + ilike(simplifiedDocumentsView.title, searchTerm), + ilike(simplifiedDocumentsView.docNumber, searchTerm), + ilike(simplifiedDocumentsView.vendorDocNumber, searchTerm), + ) + } + + // 5. 최종 WHERE 조건 (계약 ID들로 필터링) + const finalWhere = and( + advancedWhere, + globalWhere, + ) + + // 6. 정렬 처리 + const orderBy = input.sort && input.sort.length > 0 + ? input.sort.map((item) => + item.desc + ? desc(simplifiedDocumentsView[item.id]) + : asc(simplifiedDocumentsView[item.id]) + ) + : [desc(simplifiedDocumentsView.createdAt)] + + // 7. 트랜잭션 실행 + const { data, total, drawingKind, vendorInfo } = await db.transaction(async (tx) => { + // 데이터 조회 + const data = await tx + .select() + .from(simplifiedDocumentsView) + .where(finalWhere) + .orderBy(...orderBy) + .limit(input.perPage) + .offset(offset) + + // 총 개수 조회 + const [{ total }] = await tx + .select({ + total: count() + }) + .from(simplifiedDocumentsView) + .where(finalWhere) + + // DrawingKind 분석 (첫 번째 문서의 drawingKind 사용) + const drawingKind = data.length > 0 ? data[0].drawingKind : null + + // 벤더 정보 조회 + + + return { data, total, drawingKind } + }) + + const pageCount = Math.ceil(total / input.perPage) + + return { + data, + pageCount, + total, + drawingKind: drawingKind as 'B3' | 'B4' | 'B5' | null, + } + } catch (err) { + console.error("Error fetching user vendor documents:", err) + return { data: [], pageCount: 0, total: 0, drawingKind: null } + } + } + + /** + * DrawingKind별 문서 통계 조회 + */ + export async function getUserVendorDocumentStatsAll(userId: number) { + try { + + const session = await getServerSession(authOptions) + if (!session?.user?.id) { + throw new Error("인증이 필요합니다.") + } + + + // DrawingKind별 통계 조회 + const documents = await db + .select({ + drawingKind: simplifiedDocumentsView.drawingKind, + }) + .from(simplifiedDocumentsView) + + // 통계 계산 + const stats = documents.reduce((acc, doc) => { + if (doc.drawingKind) { + acc[doc.drawingKind] = (acc[doc.drawingKind] || 0) + 1 + } + return acc + }, {} as Record<string, number>) + + // 가장 많은 DrawingKind 찾기 + const primaryDrawingKind = Object.entries(stats) + .sort(([,a], [,b]) => b - a)[0]?.[0] as 'B3' | 'B4' | 'B5' | undefined + + return { + stats, + totalDocuments: documents.length, + primaryDrawingKind: primaryDrawingKind || null + } + } catch (err) { + console.error("Error fetching user vendor document stats:", err) + return { stats: {}, totalDocuments: 0, primaryDrawingKind: null } + } + } export interface UpdateRevisionInput { diff --git a/lib/vendor-document-list/import-service.ts b/lib/vendor-document-list/import-service.ts index 216e373e..61670c79 100644 --- a/lib/vendor-document-list/import-service.ts +++ b/lib/vendor-document-list/import-service.ts @@ -1076,6 +1076,7 @@ class ImportService { // DOLCE 상세 데이터를 revisions 스키마에 맞게 변환 const revisionData = { + serialNo:detailDoc.RegisterSerialNo , issueStageId, revision: detailDoc.DrawingRevNo, uploaderType, @@ -1096,6 +1097,7 @@ class ImportService { existingRevision.revision !== revisionData.revision || existingRevision.revisionStatus !== revisionData.revisionStatus || existingRevision.uploaderName !== revisionData.uploaderName || + existingRevision.serialNo !== revisionData.serialNo || existingRevision.registerId !== revisionData.registerId // 🆕 registerId 변경 확인 if (hasChanges) { diff --git a/lib/vendor-document-list/ship-all/enhanced-doc-table-columns.tsx b/lib/vendor-document-list/ship-all/enhanced-doc-table-columns.tsx new file mode 100644 index 00000000..6c9a9ab6 --- /dev/null +++ b/lib/vendor-document-list/ship-all/enhanced-doc-table-columns.tsx @@ -0,0 +1,540 @@ +// simplified-doc-table-columns.tsx +"use client" + +import * as React from "react" +import { ColumnDef } from "@tanstack/react-table" +import { formatDate, formatDateTime } from "@/lib/utils" +import { Checkbox } from "@/components/ui/checkbox" +import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" +import { DataTableRowAction } from "@/types/table" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { Button } from "@/components/ui/button" +import { + Ellipsis, + FileText, + Eye, + Edit, + Trash2, +} from "lucide-react" +import { cn } from "@/lib/utils" +import { SimplifiedDocumentsView } from "@/db/schema" + +// DocumentSelectionContext를 import (실제 파일 경로에 맞게 수정 필요) +// 예: import { DocumentSelectionContextAll } from "../user-vendor-document-display" +// 또는: import { DocumentSelectionContextAll } from "./user-vendor-document-display" +import { DocumentSelectionContextAll } from "@/components/ship-vendor-document-all/user-vendor-document-table-container" + +interface GetColumnsProps { + setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<SimplifiedDocumentsView> | null>> +} + +// 날짜 표시 컴포넌트 (간단 버전) +const DateDisplay = ({ date, isSelected = false }: { date: string | null, isSelected?: boolean }) => { + if (!date) return <span className="text-gray-400">-</span> + + return ( + <span className={cn( + "text-sm", + isSelected && "text-blue-600 font-semibold" + )}> + {formatDate(date)} + </span> + ) +} + +export function getSimplifiedDocumentColumns({ + setRowAction, +}: GetColumnsProps): ColumnDef<SimplifiedDocumentsView>[] { + + const columns: ColumnDef<SimplifiedDocumentsView>[] = [ + // 라디오 버튼 같은 체크박스 선택 + { + id: "select", + header: ({ table }) => ( + <div className="flex items-center justify-center"> + <span className="text-xs text-gray-500">Select</span> + </div> + ), + cell: ({ row }) => { + const doc = row.original + + return ( + <SelectCell documentId={doc.documentId} /> + ) + }, + size: 40, + enableSorting: false, + enableHiding: false, + }, + + // 문서번호 (선택된 행 하이라이트 적용) + { + accessorKey: "docNumber", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="Document No" /> + ), + cell: ({ row }) => { + const doc = row.original + + return ( + <DocNumberCell doc={doc} /> + ) + }, + size: 120, + enableResizing: true, + meta: { + excelHeader: "Document No" + }, + }, + + // 문서명 (선택된 행 하이라이트 적용) + { + accessorKey: "title", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="Title" /> + ), + cell: ({ row }) => { + const doc = row.original + + return ( + <TitleCell doc={doc} /> + ) + }, + enableResizing: true, + maxSize:300, + meta: { + excelHeader: "Title" + }, + }, + + // 프로젝트 코드 + { + accessorKey: "projectCode", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="Project" /> + ), + cell: ({ row }) => { + const projectCode = row.original.projectCode + + return ( + <ProjectCodeCell projectCode={projectCode} documentId={row.original.documentId} /> + ) + }, + enableResizing: true, + maxSize:100, + meta: { + excelHeader: "Project" + }, + }, + + // 벤더명 + { + accessorKey: "vendorName", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="Vendor Name" /> + ), + cell: ({ row }) => { + const vendorName = row.original.vendorName + + return ( + <VendorNameCell vendorName={vendorName} documentId={row.original.documentId} /> + ) + }, + enableResizing: true, + maxSize: 200, + meta: { + excelHeader: "Vendor Name" + }, + }, + + // 벤더 코드 + { + accessorKey: "vendorCode", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="Vendor Code" /> + ), + cell: ({ row }) => { + const vendorCode = row.original.vendorCode + + return ( + <VendorCodeCell vendorCode={vendorCode} documentId={row.original.documentId} /> + ) + }, + enableResizing: true, + maxSize: 120, + meta: { + excelHeader: "Vendor Code" + }, + }, + + // 1차 스테이지 그룹 + { + id: "firstStageGroup", + header: ({ table }) => { + // 첫 번째 행의 firstStageName을 그룹 헤더로 사용 + const firstRow = table.getRowModel().rows[0]?.original + const stageName = firstRow?.firstStageName || "First Stage" + return ( + <div className="text-center font-medium text-gray-700"> + {stageName} + </div> + ) + }, + columns: [ + { + accessorKey: "firstStagePlanDate", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="Planned Date" /> + ), + cell: ({ row }) => { + return <FirstStagePlanDateCell row={row} /> + }, + enableResizing: true, + meta: { + excelHeader: "First Planned Date" + }, + }, + { + accessorKey: "firstStageActualDate", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="Actual Date" /> + ), + cell: ({ row }) => { + return <FirstStageActualDateCell row={row} /> + }, + enableResizing: true, + meta: { + excelHeader: "First Actual Date" + }, + }, + ], + }, + + // 2차 스테이지 그룹 + { + id: "secondStageGroup", + header: ({ table }) => { + // 첫 번째 행의 secondStageName을 그룹 헤더로 사용 + const firstRow = table.getRowModel().rows[0]?.original + const stageName = firstRow?.secondStageName || "Second Stage" + return ( + <div className="text-center font-medium text-gray-700"> + {stageName} + </div> + ) + }, + columns: [ + { + accessorKey: "secondStagePlanDate", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="Planned Date" /> + ), + cell: ({ row }) => { + return <SecondStagePlanDateCell row={row} /> + }, + enableResizing: true, + meta: { + excelHeader: "Second Planned Date" + }, + }, + { + accessorKey: "secondStageActualDate", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="Actual Date" /> + ), + cell: ({ row }) => { + return <SecondStageActualDateCell row={row} /> + }, + enableResizing: true, + meta: { + excelHeader: "Second Actual Date" + }, + }, + ], + }, + + // 첨부파일 수 + { + accessorKey: "attachmentCount", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="Files" /> + ), + cell: ({ row }) => { + const count = row.original.attachmentCount || 0 + + return ( + <AttachmentCountCell count={count} documentId={row.original.documentId} /> + ) + }, + size: 60, + enableResizing: true, + meta: { + excelHeader: "Attachments" + }, + }, + + // 업데이트 일시 + { + accessorKey: "updatedAt", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="Updated" /> + ), + cell: ({ cell, row }) => { + return ( + <UpdatedAtCell updatedAt={cell.getValue() as Date} documentId={row.original.documentId} /> + ) + }, + enableResizing: true, + meta: { + excelHeader: "Updated" + }, + }, + + // 액션 버튼 + // { + // id: "actions", + // header: () => <span className="sr-only">Actions</span>, + // cell: ({ row }) => { + // const doc = row.original + // return ( + // <DropdownMenu> + // <DropdownMenuTrigger asChild> + // <Button variant="ghost" className="h-8 w-8 p-0"> + // <span className="sr-only">Open menu</span> + // <Ellipsis className="h-4 w-4" /> + // </Button> + // </DropdownMenuTrigger> + // <DropdownMenuContent align="end"> + // <DropdownMenuItem + // onClick={() => setRowAction({ type: "view", row: doc })} + // > + // <Eye className="mr-2 h-4 w-4" /> + // 보기 + // </DropdownMenuItem> + // <DropdownMenuItem + // onClick={() => setRowAction({ type: "edit", row: doc })} + // > + // <Edit className="mr-2 h-4 w-4" /> + // 편집 + // </DropdownMenuItem> + // <DropdownMenuSeparator /> + // <DropdownMenuItem + // onClick={() => setRowAction({ type: "delete", row: doc })} + // className="text-red-600" + // > + // <Trash2 className="mr-2 h-4 w-4" /> + // 삭제 + // <DropdownMenuShortcut>⌫</DropdownMenuShortcut> + // </DropdownMenuItem> + // </DropdownMenuContent> + // </DropdownMenu> + // ) + // }, + // size: 50, + // enableSorting: false, + // enableHiding: false, + // }, + ] + + return columns +} + +// 개별 셀 컴포넌트들 (Context 사용) +function SelectCell({ documentId }: { documentId: number }) { + const { selectedDocumentId, setSelectedDocumentId } = React.useContext(DocumentSelectionContextAll); + const isSelected = selectedDocumentId === documentId; + + return ( + <div className="flex items-center justify-center"> + <input + type="radio" + checked={isSelected} + onChange={() => { + const newSelection = isSelected ? null : documentId; + setSelectedDocumentId(newSelection); + }} + className="cursor-pointer w-4 h-4" + /> + </div> + ); +} + +function DocNumberCell({ doc }: { doc: SimplifiedDocumentsView }) { + const { selectedDocumentId, setSelectedDocumentId } = React.useContext(DocumentSelectionContextAll); + const isSelected = selectedDocumentId === doc.documentId; + + return ( + <div + className={cn( + "font-mono text-sm font-medium cursor-pointer px-2 py-1 rounded transition-colors", + isSelected + ? "text-blue-600 font-bold bg-blue-50" + : "hover:bg-gray-50" + )} + onClick={() => { + const newSelection = isSelected ? null : doc.documentId; + setSelectedDocumentId(newSelection); + }} + > + {doc.docNumber} + </div> + ); +} + +function TitleCell({ doc }: { doc: SimplifiedDocumentsView }) { + const { selectedDocumentId, setSelectedDocumentId } = React.useContext(DocumentSelectionContextAll); + const isSelected = selectedDocumentId === doc.documentId; + + return ( + <div + className={cn( + "font-medium text-gray-900 truncate max-w-[300px] cursor-pointer px-2 py-1 rounded transition-colors", + isSelected + ? "text-blue-600 font-bold bg-blue-50" + : "hover:bg-gray-50" + )} + title={doc.title} + onClick={() => { + const newSelection = isSelected ? null : doc.documentId; + setSelectedDocumentId(newSelection); + }} + > + {doc.title} + </div> + ); +} + +function ProjectCodeCell({ projectCode, documentId }: { projectCode: string | null, documentId: number }) { + const { selectedDocumentId } = React.useContext(DocumentSelectionContextAll); + const isSelected = selectedDocumentId === documentId; + + if (!projectCode) return <span className="text-gray-400">-</span>; + + return ( + <span className={cn( + "text-sm font-medium", + isSelected && "text-blue-600 font-bold" + )}> + {projectCode} + </span> + ); +} + +function VendorNameCell({ vendorName, documentId }: { vendorName: string | null, documentId: number }) { + const { selectedDocumentId } = React.useContext(DocumentSelectionContextAll); + const isSelected = selectedDocumentId === documentId; + + if (!vendorName) return <span className="text-gray-400">-</span>; + + return ( + <div + className={cn( + "text-sm font-medium truncate max-w-[200px]", + isSelected && "text-blue-600 font-bold" + )} + title={vendorName} + > + {vendorName} + </div> + ); +} + +function VendorCodeCell({ vendorCode, documentId }: { vendorCode: string | null, documentId: number }) { + const { selectedDocumentId } = React.useContext(DocumentSelectionContextAll); + const isSelected = selectedDocumentId === documentId; + + if (!vendorCode) return <span className="text-gray-400">-</span>; + + return ( + <span className={cn( + "text-sm font-medium font-mono", + isSelected && "text-blue-600 font-bold" + )}> + {vendorCode} + </span> + ); +} + +function FirstStagePlanDateCell({ row }: { row: any }) { + const { selectedDocumentId } = React.useContext(DocumentSelectionContextAll); + const isSelected = selectedDocumentId === row.original.documentId; + + return <DateDisplay date={row.original.firstStagePlanDate} isSelected={isSelected} />; +} + +function FirstStageActualDateCell({ row }: { row: any }) { + const { selectedDocumentId } = React.useContext(DocumentSelectionContextAll); + const isSelected = selectedDocumentId === row.original.documentId; + const date = row.original.firstStageActualDate; + + return ( + <div className={cn( + date ? "text-green-600 font-medium" : "", + isSelected && date && "text-green-700 font-bold" + )}> + <DateDisplay date={date} isSelected={isSelected && !date} /> + {date && <span className="text-xs block">✓ 완료</span>} + </div> + ); +} + +function SecondStagePlanDateCell({ row }: { row: any }) { + const { selectedDocumentId } = React.useContext(DocumentSelectionContextAll); + const isSelected = selectedDocumentId === row.original.documentId; + + return <DateDisplay date={row.original.secondStagePlanDate} isSelected={isSelected} />; +} + +function SecondStageActualDateCell({ row }: { row: any }) { + const { selectedDocumentId } = React.useContext(DocumentSelectionContextAll); + const isSelected = selectedDocumentId === row.original.documentId; + const date = row.original.secondStageActualDate; + + return ( + <div className={cn( + date ? "text-green-600 font-medium" : "", + isSelected && date && "text-green-700 font-bold" + )}> + <DateDisplay date={date} isSelected={isSelected && !date} /> + {date && <span className="text-xs block">✓ 완료</span>} + </div> + ); +} + +function AttachmentCountCell({ count, documentId }: { count: number, documentId: number }) { + const { selectedDocumentId } = React.useContext(DocumentSelectionContextAll); + const isSelected = selectedDocumentId === documentId; + + return ( + <div className="flex items-center justify-center gap-1"> + <FileText className="w-4 h-4 text-gray-400" /> + <span className={cn( + "text-sm font-medium", + isSelected && "text-blue-600 font-bold" + )}> + {count} + </span> + </div> + ); +} + +function UpdatedAtCell({ updatedAt, documentId }: { updatedAt: Date, documentId: number }) { + const { selectedDocumentId } = React.useContext(DocumentSelectionContextAll); + const isSelected = selectedDocumentId === documentId; + + return ( + <span className={cn( + "text-sm text-gray-600", + isSelected && "text-blue-600 font-semibold" + )}> + {formatDateTime(updatedAt)} + </span> + ); +}
\ No newline at end of file diff --git a/lib/vendor-document-list/ship-all/enhanced-documents-table.tsx b/lib/vendor-document-list/ship-all/enhanced-documents-table.tsx new file mode 100644 index 00000000..255aa56c --- /dev/null +++ b/lib/vendor-document-list/ship-all/enhanced-documents-table.tsx @@ -0,0 +1,296 @@ +// simplified-documents-table.tsx - 최적화된 버전 +"use client" + +import React from "react" +import type { + DataTableAdvancedFilterField, + DataTableFilterField, + DataTableRowAction, +} from "@/types/table" + +import { useDataTable } from "@/hooks/use-data-table" +import { getUserVendorDocumentsAll, getUserVendorDocumentStatsAll } from "@/lib/vendor-document-list/enhanced-document-service" +import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar" +import { toast } from "sonner" +import { Badge } from "@/components/ui/badge" +import { FileText } from "lucide-react" + +import { Label } from "@/components/ui/label" +import { DataTable } from "@/components/data-table/data-table" +import { SimplifiedDocumentsView } from "@/db/schema" +import { getSimplifiedDocumentColumns } from "./enhanced-doc-table-columns" +// import { EnhancedDocTableToolbarActions } from "./enhanced-doc-table-toolbar-actions" + +// DrawingKind별 설명 매핑 +const DRAWING_KIND_INFO = { + B3: { + title: "B3 Vendor", + description: "Approval-focused drawings progressing through Approval → Work stages", + color: "bg-blue-50 text-blue-700 border-blue-200" + }, + B4: { + title: "B4 GTT", + description: "DOLCE-integrated drawings progressing through Pre → Work stages", + color: "bg-green-50 text-green-700 border-green-200" + }, + B5: { + title: "B5 FMEA", + description: "Sequential drawings progressing through First → Second stages", + color: "bg-purple-50 text-purple-700 border-purple-200" + } +} as const + +interface SimplifiedDocumentsTableProps { + allPromises: Promise<[ + Awaited<ReturnType<typeof getUserVendorDocumentsAll>>, + Awaited<ReturnType<typeof getUserVendorDocumentStatsAll>> + ]> + onDataLoaded?: (data: SimplifiedDocumentsView[]) => void +} + +export function SimplifiedDocumentsTable({ + allPromises, + onDataLoaded, +}: SimplifiedDocumentsTableProps) { + // 🔥 React.use() 결과를 안전하게 처리 + const promiseResults = React.use(allPromises) + const [documentResult, statsResult] = promiseResults + + // 🔥 데이터 구조분해를 메모이제이션 + const { data, pageCount, total, drawingKind, vendorInfo } = React.useMemo(() => documentResult, [documentResult]) + const { stats, totalDocuments, primaryDrawingKind } = React.useMemo(() => statsResult, [statsResult]) + + // 🔥 데이터 로드 콜백을 useCallback으로 최적화 + const handleDataLoaded = React.useCallback((loadedData: SimplifiedDocumentsView[]) => { + onDataLoaded?.(loadedData) + }, [onDataLoaded]) + + // 🔥 데이터가 로드되면 콜백 호출 (의존성 최적화) + React.useEffect(() => { + if (data && handleDataLoaded) { + handleDataLoaded(data) + } + }, [data, handleDataLoaded]) + + // 🔥 상태들을 안정적으로 관리 + const [rowAction, setRowAction] = React.useState<DataTableRowAction<SimplifiedDocumentsView> | null>(null) + const [expandedRows] = React.useState<Set<string>>(() => new Set()) + + // 🔥 컬럼 메모이제이션 최적화 + const columns = React.useMemo( + () => getSimplifiedDocumentColumns({ + setRowAction, + }), + [] // setRowAction은 항상 동일한 함수이므로 의존성에서 제외 + ) + + // 🔥 필터 필드들을 메모이제이션 + const advancedFilterFields: DataTableAdvancedFilterField<SimplifiedDocumentsView>[] = React.useMemo(() => [ + { + id: "docNumber", + label: "Document No", + type: "text", + }, + { + id: "title", + label: "Document Title", + type: "text", + }, + { + id: "drawingKind", + label: "Document Type", + type: "select", + options: [ + { label: "B3", value: "B3" }, + { label: "B4", value: "B4" }, + { label: "B5", value: "B5" }, + ], + }, + { + id: "projectCode", + label: "Project Code", + type: "text", + }, + { + id: "vendorName", + label: "Vendor Name", + type: "text", + }, + { + id: "vendorCode", + label: "Vendor Code", + type: "text", + }, + { + id: "pic", + label: "PIC", + type: "text", + }, + { + id: "status", + label: "Document Status", + type: "select", + options: [ + { label: "Active", value: "ACTIVE" }, + { label: "Inactive", value: "INACTIVE" }, + { label: "Pending", value: "PENDING" }, + { label: "Completed", value: "COMPLETED" }, + ], + }, + { + id: "firstStageName", + label: "First Stage", + type: "text", + }, + { + id: "secondStageName", + label: "Second Stage", + type: "text", + }, + { + id: "firstStagePlanDate", + label: "First Planned Date", + type: "date", + }, + { + id: "firstStageActualDate", + label: "First Actual Date", + type: "date", + }, + { + id: "secondStagePlanDate", + label: "Second Planned Date", + type: "date", + }, + { + id: "secondStageActualDate", + label: "Second Actual Date", + type: "date", + }, + { + id: "issuedDate", + label: "Issue Date", + type: "date", + }, + { + id: "createdAt", + label: "Created Date", + type: "date", + }, + { + id: "updatedAt", + label: "Updated Date", + type: "date", + }, + ], []) + + // 🔥 B4 전용 필드들 메모이제이션 + const b4FilterFields: DataTableAdvancedFilterField<SimplifiedDocumentsView>[] = React.useMemo(() => [ + { + id: "cGbn", + label: "C Category", + type: "text", + }, + { + id: "dGbn", + label: "D Category", + type: "text", + }, + { + id: "degreeGbn", + label: "Degree Category", + type: "text", + }, + { + id: "deptGbn", + label: "Dept Category", + type: "text", + }, + { + id: "jGbn", + label: "J Category", + type: "text", + }, + { + id: "sGbn", + label: "S Category", + type: "text", + }, + ], []) + + // 🔥 B4 문서 존재 여부 체크 메모이제이션 + const hasB4Documents = React.useMemo(() => { + return data.some(doc => doc.drawingKind === 'B4') + }, [data]) + + // 🔥 최종 필터 필드 메모이제이션 + const finalFilterFields = React.useMemo(() => { + return hasB4Documents ? [...advancedFilterFields, ...b4FilterFields] : advancedFilterFields + }, [hasB4Documents, advancedFilterFields, b4FilterFields]) + + // 🔥 테이블 초기 상태 메모이제이션 + const tableInitialState = React.useMemo(() => ({ + sorting: [{ id: "createdAt", desc: true }], + columnPinning: { right: ["actions"] }, + }), []) + + // 🔥 getRowId 함수 메모이제이션 + const getRowId = React.useCallback((originalRow: SimplifiedDocumentsView) => String(originalRow.documentId), []) + + const { table } = useDataTable({ + data, + columns, + pageCount, + enablePinning: true, + enableAdvancedFilter: true, + initialState: tableInitialState, + getRowId, + shallow: false, + clearOnDefault: true, + columnResizeMode: "onEnd", + }) + + // 🔥 활성 drawingKind 메모이제이션 + const activeDrawingKind = React.useMemo(() => { + return drawingKind || primaryDrawingKind + }, [drawingKind, primaryDrawingKind]) + + // 🔥 kindInfo 메모이제이션 + const kindInfo = React.useMemo(() => { + return activeDrawingKind ? DRAWING_KIND_INFO[activeDrawingKind] : null + }, [activeDrawingKind]) + + return ( + <div className="w-full space-y-4"> + {/* DrawingKind 정보 간단 표시 */} + {kindInfo && ( + <div className="flex items-center justify-between"> + <div className="flex items-center gap-4"> + {/* 주석 처리된 부분은 그대로 유지 */} + </div> + <div className="flex items-center gap-2"> + <Badge variant="outline"> + {total} documents + </Badge> + </div> + </div> + )} + + {/* 테이블 */} + <div className="overflow-x-auto"> + <DataTable table={table} compact> + <DataTableAdvancedToolbar + table={table} + filterFields={finalFilterFields} + shallow={false} + > + {/* <EnhancedDocTableToolbarActions + table={table} + projectType="ship" + /> */} + </DataTableAdvancedToolbar> + </DataTable> + </div> + </div> + ) +}
\ No newline at end of file diff --git a/lib/vendor-document-list/ship/import-from-dolce-button.tsx b/lib/vendor-document-list/ship/import-from-dolce-button.tsx index 1ffe466d..5e720220 100644 --- a/lib/vendor-document-list/ship/import-from-dolce-button.tsx +++ b/lib/vendor-document-list/ship/import-from-dolce-button.tsx @@ -223,6 +223,8 @@ export function ImportFromDOLCEButton({ } }, [debouncedProjectIds, fetchAllImportStatus]) + + // 🔥 전체 통계 메모이제이션 const totalStats = React.useMemo(() => { const statuses = Array.from(importStatusMap.values()) @@ -389,6 +391,42 @@ export function ImportFromDOLCEButton({ fetchAllImportStatus() }, [fetchAllImportStatus]) + + // 🔥 자동 동기화 실행 (기존 useEffect들 다음에 추가) + React.useEffect(() => { + // 조건: 가져오기 가능하고, 동기화할 항목이 있고, 현재 진행중이 아닐 때 + if (canImport && + (totalStats.newDocuments > 0 || totalStats.updatedDocuments > 0) && + !isImporting && + !isDialogOpen) { + + // 상태 로딩이 완료된 후 잠깐 대기 (사용자가 상태를 확인할 수 있도록) + const timer = setTimeout(() => { + console.log(`🔄 자동 동기화 시작: 새 문서 ${totalStats.newDocuments}개, 업데이트 ${totalStats.updatedDocuments}개`) + + // 동기화 시작 알림 + toast.info( + '새로운 문서가 발견되어 자동 동기화를 시작합니다', + { + description: `새 문서 ${totalStats.newDocuments}개, 업데이트 ${totalStats.updatedDocuments}개`, + duration: 3000 + } + ) + + // 자동으로 다이얼로그 열고 동기화 실행 + setIsDialogOpen(true) + + // 잠깐 후 실제 동기화 시작 (다이얼로그가 열리는 시간) + setTimeout(() => { + handleImport() + }, 500) + }, 1500) // 1.5초 대기 + + return () => clearTimeout(timer) + } + }, [canImport, totalStats.newDocuments, totalStats.updatedDocuments, isImporting, isDialogOpen, handleImport]) + + // 로딩 중이거나 projectIds가 없으면 버튼을 표시하지 않음 if (projectIds.length === 0) { return null |
