summaryrefslogtreecommitdiff
path: root/lib/vendor-document-list
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-08-21 06:57:36 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-08-21 06:57:36 +0000
commit02b1cf005cf3e1df64183d20ba42930eb2767a9f (patch)
treee932c54d5260b0e6fda2b46be2a6ba1c3ee30434 /lib/vendor-document-list
parentd78378ecd7ceede1429359f8058c7a99ac34b1b7 (diff)
(대표님, 최겸) 설계메뉴추가, 작업사항 업데이트
설계메뉴 - 문서관리 설계메뉴 - 벤더 데이터 gtc 메뉴 업데이트 정보시스템 - 메뉴리스트 및 정보 업데이트 파일 라우트 업데이트 엑셀임포트 개선 기본계약 개선 벤더 가입과정 변경 및 개선 벤더 기본정보 - pq 돌체 오류 수정 및 개선 벤더 로그인 과정 이메일 오류 수정
Diffstat (limited to 'lib/vendor-document-list')
-rw-r--r--lib/vendor-document-list/dolce-upload-service.ts3
-rw-r--r--lib/vendor-document-list/enhanced-document-service.ts133
-rw-r--r--lib/vendor-document-list/import-service.ts2
-rw-r--r--lib/vendor-document-list/ship-all/enhanced-doc-table-columns.tsx540
-rw-r--r--lib/vendor-document-list/ship-all/enhanced-documents-table.tsx296
-rw-r--r--lib/vendor-document-list/ship/import-from-dolce-button.tsx38
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