summaryrefslogtreecommitdiff
path: root/lib/vendor-document-list/ship
diff options
context:
space:
mode:
Diffstat (limited to 'lib/vendor-document-list/ship')
-rw-r--r--lib/vendor-document-list/ship/bulk-b4-upload-dialog.tsx428
-rw-r--r--lib/vendor-document-list/ship/enhanced-doc-table-columns.tsx37
-rw-r--r--lib/vendor-document-list/ship/enhanced-doc-table-toolbar-actions.tsx77
-rw-r--r--lib/vendor-document-list/ship/enhanced-documents-table.tsx1
4 files changed, 518 insertions, 25 deletions
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>