diff options
Diffstat (limited to 'lib/vendor-document-list')
6 files changed, 760 insertions, 31 deletions
diff --git a/lib/vendor-document-list/enhanced-document-service.ts b/lib/vendor-document-list/enhanced-document-service.ts index 7464b13f..43eea6eb 100644 --- a/lib/vendor-document-list/enhanced-document-service.ts +++ b/lib/vendor-document-list/enhanced-document-service.ts @@ -24,6 +24,7 @@ import { contracts, users, vendors } from "@/db/schema" import { getServerSession } from "next-auth/next" import { authOptions } from "@/app/api/auth/[...nextauth]/route" import { countDocumentStagesOnly, selectDocumentStagesOnly } from "./repository" +import { saveFile } from "../file-stroage" // 스키마 타입 정의 export interface GetEnhancedDocumentsSchema { @@ -1468,4 +1469,234 @@ export async function getDocumentDetails(documentId: number) { error: error instanceof Error ? error.message : "Failed to delete revision" } } - }
\ No newline at end of file + } + + + interface UploadResult { + docNumber: string + revision: string + success: boolean + message?: string + error?: string + } + + interface BulkUploadResult { + success: boolean + successCount?: number + failCount?: number + results?: UploadResult[] + error?: string + } + + export async function bulkUploadB4Documents(formData: FormData): Promise<BulkUploadResult> { + try { + const session = await getServerSession(authOptions) + if (!session?.user) { + return { success: false, error: "인증이 필요합니다" } + } + + const projectId = formData.get("projectId") as string + const fileCount = parseInt(formData.get("fileCount") as string) + + if (!projectId) { + return { success: false, error: "프로젝트를 선택해주세요" } + } + + const results: UploadResult[] = [] + let successCount = 0 + let failCount = 0 + + // 문서번호별로 그룹화 + const fileGroups = new Map<string, Array<{ + file: File + revision: string + index: number + }>>() + + // 파일들을 문서번호별로 그룹화 + for (let i = 0; i < fileCount; i++) { + const file = formData.get(`file_${i}`) as File + const docNumber = formData.get(`docNumber_${i}`) as string + const revision = formData.get(`revision_${i}`) as string + + if (!file || !docNumber) continue + + if (!fileGroups.has(docNumber)) { + fileGroups.set(docNumber, []) + } + + fileGroups.get(docNumber)!.push({ + file, + revision: revision || "00", + index: i + }) + } + + // 각 문서번호 그룹 처리 + for (const [docNumber, files] of fileGroups.entries()) { + try { + // 문서가 존재하는지 확인 + const existingDoc = await db.query.documents.findFirst({ + where: and( + eq(documents.docNumber, docNumber), + eq(documents.projectId, parseInt(projectId)) + ) + }) + + if (!existingDoc) { + // 문서가 없으면 모든 파일 스킵 + for (const fileInfo of files) { + results.push({ + docNumber, + revision: fileInfo.revision, + success: false, + error: `문서번호 ${docNumber}가 존재하지 않습니다` + }) + failCount++ + } + continue + } + + // 기존 스테이지 조회 + const existingStages = await db.query.issueStages.findMany({ + where: eq(issueStages.documentId, existingDoc.id) + }) + + const preStage = existingStages.find(s => s.stageName === 'GTT → SHI (For Pre.DWG)') + const workStage = existingStages.find(s => s.stageName === 'GTT → SHI (For Work.DWG)') + + // 파일별 처리 (첫 번째 리비전은 Pre.DWG, 나머지는 Work.DWG) + for (let fileIndex = 0; fileIndex < files.length; fileIndex++) { + const fileInfo = files[fileIndex] + let targetStageId: number + + try { + // 스테이지 결정 및 생성 + if (fileIndex === 0) { + // 첫 번째 리비전 - Pre.DWG 스테이지 + if (preStage) { + targetStageId = preStage.id + } else { + // Pre.DWG 스테이지 생성 + const [newStage] = await db.insert(issueStages).values({ + documentId: existingDoc.id, + stageName: 'GTT → SHI (For Pre.DWG)', + stageOrder: 1, + stageStatus: 'PLANNED', + }).returning() + targetStageId = newStage.id + } + } else { + // 나머지 리비전 - Work.DWG 스테이지 + if (workStage) { + targetStageId = workStage.id + } else { + // Work.DWG 스테이지 생성 + const [newStage] = await db.insert(issueStages).values({ + documentId: existingDoc.id, + stageName: 'GTT → SHI (For Work.DWG)', + stageOrder: 2, + stageStatus: 'PLANNED', + }).returning() + targetStageId = newStage.id + } + } + + // 같은 리비전이 이미 있는지 확인 + const existingRevision = await db.query.revisions.findFirst({ + where: and( + eq(revisions.issueStageId, targetStageId), + eq(revisions.revision, fileInfo.revision) + ) + }) + + let revisionId: number + + if (existingRevision) { + // 기존 리비전 사용 + revisionId = existingRevision.id + } else { + // 새 리비전 생성 + const [newRevision] = await db.insert(revisions).values({ + issueStageId: targetStageId, + revision: fileInfo.revision, + uploaderType: "vendor", + uploaderName: session.user.name || "System", + uploadedAt: new Date().toISOString().split('T')[0], + submittedDate: new Date().toISOString().split('T')[0], + revisionStatus: 'SUBMITTED', + }).returning() + revisionId = newRevision.id + } + + // 파일 저장 + const saveResult = await saveFile({ + file: fileInfo.file, + directory: `documents/${existingDoc.id}/revisions/${revisionId}`, + originalName: fileInfo.file.name, + userId: session.user.id + }) + + if (!saveResult.success) { + throw new Error(saveResult.error || "파일 저장 실패") + } + + // 첨부파일 정보 저장 + await db.insert(documentAttachments).values({ + revisionId, + fileName: fileInfo.file.name, + filePath: saveResult.publicPath!, + fileType: fileInfo.file.type, + fileSize: fileInfo.file.size, + }) + + results.push({ + docNumber, + revision: fileInfo.revision, + success: true, + message: `${fileIndex === 0 ? 'Pre.DWG' : 'Work.DWG'} 스테이지에 업로드 완료` + }) + successCount++ + + } catch (fileError) { + results.push({ + docNumber, + revision: fileInfo.revision, + success: false, + error: fileError instanceof Error ? fileError.message : "파일 처리 실패" + }) + failCount++ + } + } + + } catch (docError) { + // 문서 그룹 전체 에러 + for (const fileInfo of files) { + results.push({ + docNumber, + revision: fileInfo.revision, + success: false, + error: docError instanceof Error ? docError.message : "문서 처리 실패" + }) + failCount++ + } + } + } + + revalidatePath('/documents') + + return { + success: true, + successCount, + failCount, + results + } + + } catch (error) { + console.error("Bulk upload error:", error) + return { + success: false, + error: error instanceof Error ? error.message : "업로드 중 오류가 발생했습니다" + } + } + } diff --git a/lib/vendor-document-list/import-service.ts b/lib/vendor-document-list/import-service.ts index 61670c79..135dfb3a 100644 --- a/lib/vendor-document-list/import-service.ts +++ b/lib/vendor-document-list/import-service.ts @@ -657,7 +657,8 @@ class ImportService { .from(documents) .where(and( eq(documents.projectId, projectId), - eq(documents.docNumber, dolceDoc.DrawingNo) + eq(documents.docNumber, dolceDoc.DrawingNo), + eq(documents.discipline, dolceDoc.Discipline) )) .limit(1) @@ -765,7 +766,8 @@ class ImportService { .from(documents) .where(and( eq(documents.projectId, projectId), - eq(documents.docNumber, dolceDoc.DrawingNo) + eq(documents.docNumber, dolceDoc.DrawingNo), + eq(documents.discipline, dolceDoc.Discipline), )) .limit(1) @@ -1275,7 +1277,8 @@ class ImportService { .from(documents) .where(and( eq(documents.projectId, projectId), - eq(documents.docNumber, drawingNo) + eq(documents.docNumber, drawingNo), + eq(documents.discipline, dolceDoc.Discipline) )) .limit(1) @@ -1340,7 +1343,8 @@ class ImportService { .from(documents) .where(and( eq(documents.projectId, projectId), - eq(documents.docNumber, drawingNo) + eq(documents.docNumber, drawingNo), + eq(documents.discipline, dolceDoc.Discipline) )) .limit(1) @@ -1403,7 +1407,8 @@ class ImportService { .from(documents) .where(and( eq(documents.projectId, projectId), - eq(documents.docNumber, drawingNo) + eq(documents.docNumber, drawingNo), + eq(documents.discipline, dolceDoc.Discipline) )) .limit(1) diff --git a/lib/vendor-document-list/ship/bulk-b4-upload-dialog.tsx b/lib/vendor-document-list/ship/bulk-b4-upload-dialog.tsx new file mode 100644 index 00000000..65166bd6 --- /dev/null +++ b/lib/vendor-document-list/ship/bulk-b4-upload-dialog.tsx @@ -0,0 +1,428 @@ +"use client" + +import * as React from "react" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { z } from "zod" +import { toast } from "sonner" +import { useRouter } from "next/navigation" + +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { ScrollArea } from "@/components/ui/scroll-area" +import { Separator } from "@/components/ui/separator" +import { Badge } from "@/components/ui/badge" +import { + Upload, + X, + Loader2, + FileSpreadsheet, + Files, + CheckCircle2, + AlertCircle, + AlertTriangle, + FileText, +} from "lucide-react" + +import { SimplifiedDocumentsView } from "@/db/schema" +import { bulkUploadB4Documents } from "../enhanced-document-service" + +// 파일명 파싱 유틸리티 +function parseFileName(fileName: string): { docNumber: string | null; revision: string | null } { + // 파일 확장자 제거 + const nameWithoutExt = fileName.replace(/\.[^.]+$/, "") + + // revision 패턴 찾기 (R01, r01, REV01, rev01 등) + const revisionMatch = nameWithoutExt.match(/[Rr](?:EV)?(\d+)/g) + const revision = revisionMatch ? revisionMatch[revisionMatch.length - 1].toUpperCase() : null + + // revision 제거한 나머지에서 docNumber 찾기 + let cleanedName = nameWithoutExt + if (revision) { + // revision과 그 앞의 구분자를 제거 + const revPattern = new RegExp(`[-_\\s]*${revision.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1")}.*$`, 'i') + cleanedName = cleanedName.replace(revPattern, "") + } + + // docNumber 패턴 찾기 (XX-XX-XX 형태) + // 공백이나 언더스코어를 하이픈으로 정규화 + const normalizedName = cleanedName.replace(/[\s_]+/g, '-') + + // 2~3자리 코드가 2~3개 연결된 패턴 찾기 + const docNumberPatterns = [ + /\b([A-Za-z]{2,3})-([A-Za-z]{2,3})-([A-Za-z0-9]{2,4})\b/, + /\b([A-Za-z]{2,3})\s+([A-Za-z]{2,3})\s+([A-Za-z0-9]{2,4})\b/, + ] + + let docNumber = null + for (const pattern of docNumberPatterns) { + const match = normalizedName.match(pattern) || cleanedName.match(pattern) + if (match) { + docNumber = `${match[1]}-${match[2]}-${match[3]}`.toUpperCase() + break + } + } + + return { docNumber, revision } +} + +// Form schema +const formSchema = z.object({ + projectId: z.string().min(1, "Please select a project"), + files: z.array(z.instanceof(File)).min(1, "Please select files"), +}) + +interface BulkB4UploadDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + allDocuments: SimplifiedDocumentsView[] +} + +interface ParsedFile { + file: File + docNumber: string | null + revision: string | null + status: 'pending' | 'uploading' | 'success' | 'error' | 'ignored' + message?: string +} + +export function BulkB4UploadDialog({ + open, + onOpenChange, + allDocuments +}: BulkB4UploadDialogProps) { + const [isUploading, setIsUploading] = React.useState(false) + const [parsedFiles, setParsedFiles] = React.useState<ParsedFile[]>([]) + const router = useRouter() + + // 프로젝트 ID 추출 + const projectOptions = React.useMemo(() => { + const projectIds = [...new Set(allDocuments.map(doc => doc.projectId).filter(Boolean))] + return projectIds.map(id => ({ + id: String(id), + code: allDocuments.find(doc => doc.projectId === id)?.projectCode || `Project ${id}` + })) + }, [allDocuments]) + + const form = useForm<z.infer<typeof formSchema>>({ + resolver: zodResolver(formSchema), + defaultValues: { + projectId: "", + files: [], + }, + }) + + // 파일 선택 시 파싱 + const handleFilesChange = (files: File[]) => { + const parsed = files.map(file => { + const { docNumber, revision } = parseFileName(file.name) + return { + file, + docNumber, + revision, + status: docNumber ? 'pending' as const : 'ignored' as const, + message: !docNumber ? 'docNumber를 찾을 수 없음' : undefined + } + }) + + setParsedFiles(parsed) + form.setValue("files", files) + } + + // 파일 제거 + const removeFile = (index: number) => { + const newParsedFiles = parsedFiles.filter((_, i) => i !== index) + setParsedFiles(newParsedFiles) + form.setValue("files", newParsedFiles.map(pf => pf.file)) + } + + // 업로드 처리 + async function onSubmit(values: z.infer<typeof formSchema>) { + setIsUploading(true) + + try { + // 유효한 파일만 필터링 + const validFiles = parsedFiles.filter(pf => pf.docNumber && pf.status === 'pending') + + if (validFiles.length === 0) { + toast.error("업로드 가능한 파일이 없습니다") + return + } + + // 파일별로 상태 업데이트 + setParsedFiles(prev => prev.map(pf => + pf.docNumber && pf.status === 'pending' + ? { ...pf, status: 'uploading' as const } + : pf + )) + + // FormData 생성 + const formData = new FormData() + formData.append("projectId", values.projectId) + + validFiles.forEach((pf, index) => { + formData.append(`file_${index}`, pf.file) + formData.append(`docNumber_${index}`, pf.docNumber!) + formData.append(`revision_${index}`, pf.revision || "00") + }) + + formData.append("fileCount", String(validFiles.length)) + + // 서버 액션 호출 + const result = await bulkUploadB4Documents(formData) + + if (result.success) { + // 성공한 파일들 표시 + setParsedFiles(prev => prev.map(pf => { + const uploadResult = result.results?.find(r => + r.docNumber === pf.docNumber && r.revision === (pf.revision || "00") + ) + + if (uploadResult?.success) { + return { ...pf, status: 'success' as const, message: uploadResult.message } + } else if (uploadResult) { + return { ...pf, status: 'error' as const, message: uploadResult.error } + } + return pf + })) + + toast.success(`${result.successCount}/${validFiles.length} 파일 업로드 완료`) + + // 모두 성공하면 닫기 + if (result.successCount === validFiles.length) { + setTimeout(() => { + onOpenChange(false) + router.refresh() + }, 1500) + } + } else { + toast.error(result.error || "업로드 실패") + setParsedFiles(prev => prev.map(pf => + pf.status === 'uploading' + ? { ...pf, status: 'error' as const, message: result.error } + : pf + )) + } + } catch (error) { + toast.error("업로드 중 오류가 발생했습니다") + setParsedFiles(prev => prev.map(pf => + pf.status === 'uploading' + ? { ...pf, status: 'error' as const, message: '업로드 실패' } + : pf + )) + } finally { + setIsUploading(false) + } + } + + // 다이얼로그 닫을 때 초기화 + React.useEffect(() => { + if (!open) { + form.reset() + setParsedFiles([]) + } + }, [open, form]) + + const validFileCount = parsedFiles.filter(pf => pf.docNumber).length + const ignoredFileCount = parsedFiles.filter(pf => !pf.docNumber).length + + return ( + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className="max-w-3xl"> + <DialogHeader> + <DialogTitle>B4 Document Bulk Upload</DialogTitle> + <DialogDescription> + Document numbers and revisions will be automatically extracted from file names. + Example: "agadfg de na oc R01.pdf" → Document Number: DE-NA-OC, Revision: R01 + </DialogDescription> + </DialogHeader> + + <Form {...form}> + <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> + <FormField + control={form.control} + name="projectId" + render={({ field }) => ( + <FormItem> + <FormLabel>Select Project *</FormLabel> + <Select onValueChange={field.onChange} value={field.value}> + <FormControl> + <SelectTrigger> + <SelectValue placeholder="Please select a project" /> + </SelectTrigger> + </FormControl> + <SelectContent> + {projectOptions.map(project => ( + <SelectItem key={project.id} value={project.id}> + {project.code} + </SelectItem> + ))} + </SelectContent> + </Select> + <FormMessage /> + </FormItem> + )} + /> + + <div className="space-y-2"> + <FormLabel>Select Files</FormLabel> + <div className="border-2 border-dashed rounded-lg p-6"> + <input + type="file" + multiple + accept=".pdf,.doc,.docx,.xls,.xlsx,.dwg,.dxf" + onChange={(e) => handleFilesChange(Array.from(e.target.files || []))} + className="hidden" + id="file-upload" + /> + <label + htmlFor="file-upload" + className="flex flex-col items-center justify-center cursor-pointer" + > + <Upload className="h-10 w-10 text-muted-foreground mb-2" /> + <p className="text-sm text-muted-foreground"> + Click or drag files to upload + </p> + <p className="text-xs text-muted-foreground mt-1"> + PDF, DOC, DOCX, XLS, XLSX, DWG, DXF + </p> + </label> + </div> + </div> + + {parsedFiles.length > 0 && ( + <div className="space-y-2"> + <div className="flex items-center justify-between"> + <FormLabel>Selected Files</FormLabel> + <div className="flex gap-2"> + <Badge variant="default"> + Valid: {validFileCount} + </Badge> + {ignoredFileCount > 0 && ( + <Badge variant="secondary"> + Ignored: {ignoredFileCount} + </Badge> + )} + </div> + </div> + + <ScrollArea className="h-[250px] border rounded-lg p-2"> + <div className="space-y-2"> + {parsedFiles.map((pf, index) => ( + <div + key={index} + className="flex items-center justify-between p-2 rounded-lg bg-muted/30" + > + <div className="flex items-center gap-3 flex-1"> + <FileText className="h-4 w-4 text-muted-foreground" /> + <div className="flex-1 min-w-0"> + <p className="text-sm font-medium truncate"> + {pf.file.name} + </p> + <div className="flex items-center gap-2 mt-1"> + {pf.docNumber ? ( + <> + <Badge variant="outline" className="text-xs"> + Doc: {pf.docNumber} + </Badge> + {pf.revision && ( + <Badge variant="outline" className="text-xs"> + Rev: {pf.revision} + </Badge> + )} + </> + ) : ( + <span className="text-xs text-muted-foreground"> + {pf.message} + </span> + )} + </div> + </div> + + <div className="flex items-center gap-2"> + {pf.status === 'uploading' && ( + <Loader2 className="h-4 w-4 animate-spin" /> + )} + {pf.status === 'success' && ( + <CheckCircle2 className="h-4 w-4 text-green-500" /> + )} + {pf.status === 'error' && ( + <AlertCircle className="h-4 w-4 text-red-500" /> + )} + {pf.status === 'ignored' && ( + <AlertTriangle className="h-4 w-4 text-yellow-500" /> + )} + + {!isUploading && ( + <Button + type="button" + variant="ghost" + size="sm" + onClick={() => removeFile(index)} + > + <X className="h-4 w-4" /> + </Button> + )} + </div> + </div> + </div> + ))} + </div> + </ScrollArea> + </div> + )} + + <DialogFooter> + <Button + type="button" + variant="outline" + onClick={() => onOpenChange(false)} + disabled={isUploading} + > + Cancel + </Button> + <Button + type="submit" + disabled={isUploading || validFileCount === 0 || !form.watch("projectId")} + > + {isUploading ? ( + <> + <Loader2 className="mr-2 h-4 w-4 animate-spin" /> + Uploading... + </> + ) : ( + <> + <Upload className="mr-2 h-4 w-4" /> + Upload ({validFileCount} files) + </> + )} + </Button> + </DialogFooter> + </form> + </Form> + </DialogContent> + </Dialog> + ) +}
\ No newline at end of file diff --git a/lib/vendor-document-list/ship/enhanced-doc-table-columns.tsx b/lib/vendor-document-list/ship/enhanced-doc-table-columns.tsx index 51c104dc..8370cd34 100644 --- a/lib/vendor-document-list/ship/enhanced-doc-table-columns.tsx +++ b/lib/vendor-document-list/ship/enhanced-doc-table-columns.tsx @@ -114,6 +114,43 @@ export function getSimplifiedDocumentColumns({ }, }, + { + accessorKey: "discipline", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="Discipline" /> + ), + cell: ({ row }) => { + return ( + <div className="text-center"> + {row.original.discipline} + </div> + ) + }, + enableResizing: true, + maxSize:80, + meta: { + excelHeader: "discipline" + }, + }, + + { + accessorKey: "managerENM", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="Contact" /> + ), + cell: ({ row }) => { + return ( + <div className="text-center"> + {row.original.managerENM} + </div> + ) + }, + enableResizing: true, + maxSize:80, + meta: { + excelHeader: "Contact" + }, + }, // 프로젝트 코드 { accessorKey: "projectCode", diff --git a/lib/vendor-document-list/ship/enhanced-doc-table-toolbar-actions.tsx b/lib/vendor-document-list/ship/enhanced-doc-table-toolbar-actions.tsx index 4ec57369..94252db5 100644 --- a/lib/vendor-document-list/ship/enhanced-doc-table-toolbar-actions.tsx +++ b/lib/vendor-document-list/ship/enhanced-doc-table-toolbar-actions.tsx @@ -1,4 +1,4 @@ -// enhanced-doc-table-toolbar-actions.tsx - 최적화된 버전 +// enhanced-doc-table-toolbar-actions.tsx - B4 업로드 기능 추가 버전 "use client" import * as React from "react" @@ -11,15 +11,18 @@ import { Button } from "@/components/ui/button" import { SimplifiedDocumentsView } from "@/db/schema/vendorDocu" import { SendToSHIButton } from "./send-to-shi-button" import { ImportFromDOLCEButton } from "./import-from-dolce-button" +import { BulkB4UploadDialog } from "./bulk-b4-upload-dialog" interface EnhancedDocTableToolbarActionsProps { table: Table<SimplifiedDocumentsView> projectType: "ship" | "plant" + b4: boolean } export function EnhancedDocTableToolbarActions({ table, projectType, + b4 }: EnhancedDocTableToolbarActionsProps) { const [bulkUploadDialogOpen, setBulkUploadDialogOpen] = React.useState(false) @@ -68,31 +71,55 @@ export function EnhancedDocTableToolbarActions({ }, [table]) return ( - <div className="flex items-center gap-2"> - {/* SHIP: DOLCE에서 목록 가져오기 */} - <ImportFromDOLCEButton - allDocuments={allDocuments} - projectIds={projectIds} // 🔥 미리 계산된 projectIds 전달 - onImportComplete={handleImportComplete} - /> + <> + <div className="flex items-center gap-2"> + {/* SHIP: DOLCE에서 목록 가져오기 */} + <ImportFromDOLCEButton + allDocuments={allDocuments} + projectIds={projectIds} // 🔥 미리 계산된 projectIds 전달 + onImportComplete={handleImportComplete} + /> - {/* Export 버튼 (공통) */} - <Button - variant="outline" - size="sm" - onClick={handleExport} - className="gap-2" - > - <Download className="size-4" aria-hidden="true" /> - <span className="hidden sm:inline">Export</span> - </Button> + {/* B4 일괄 업로드 버튼 - b4가 true일 때만 표시 */} + {b4 && ( + <Button + variant="outline" + size="sm" + onClick={() => setBulkUploadDialogOpen(true)} + className="gap-2" + > + <Upload className="size-4" aria-hidden="true" /> + <span className="hidden sm:inline">B4 업로드</span> + </Button> + )} - {/* Send to SHI 버튼 (공통) */} - <SendToSHIButton - documents={allDocuments} - onSyncComplete={handleSyncComplete} - projectType={projectType} - /> - </div> + {/* Export 버튼 (공통) */} + <Button + variant="outline" + size="sm" + onClick={handleExport} + className="gap-2" + > + <Download className="size-4" aria-hidden="true" /> + <span className="hidden sm:inline">Export</span> + </Button> + + {/* Send to SHI 버튼 (공통) */} + <SendToSHIButton + documents={allDocuments} + onSyncComplete={handleSyncComplete} + projectType={projectType} + /> + </div> + + {/* B4 일괄 업로드 다이얼로그 */} + {b4 && ( + <BulkB4UploadDialog + open={bulkUploadDialogOpen} + onOpenChange={setBulkUploadDialogOpen} + allDocuments={allDocuments} + /> + )} + </> ) }
\ No newline at end of file diff --git a/lib/vendor-document-list/ship/enhanced-documents-table.tsx b/lib/vendor-document-list/ship/enhanced-documents-table.tsx index 8051da7e..287df755 100644 --- a/lib/vendor-document-list/ship/enhanced-documents-table.tsx +++ b/lib/vendor-document-list/ship/enhanced-documents-table.tsx @@ -287,6 +287,7 @@ export function SimplifiedDocumentsTable({ <EnhancedDocTableToolbarActions table={table} projectType="ship" + b4={hasB4Documents} /> </DataTableAdvancedToolbar> </DataTable> |
