diff options
Diffstat (limited to 'lib/vendor-document-list/ship/revision-upload-dialog.tsx')
| -rw-r--r-- | lib/vendor-document-list/ship/revision-upload-dialog.tsx | 629 |
1 files changed, 0 insertions, 629 deletions
diff --git a/lib/vendor-document-list/ship/revision-upload-dialog.tsx b/lib/vendor-document-list/ship/revision-upload-dialog.tsx deleted file mode 100644 index 16fc9fbb..00000000 --- a/lib/vendor-document-list/ship/revision-upload-dialog.tsx +++ /dev/null @@ -1,629 +0,0 @@ -"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 { useSession } from "next-auth/react" -import { mutate } from "swr" // ✅ SWR mutate import 추가 - -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Textarea } from "@/components/ui/textarea" -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form" -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select" -import { - Dropzone, - DropzoneDescription, - DropzoneInput, - DropzoneTitle, - DropzoneUploadIcon, - DropzoneZone, -} from "@/components/ui/dropzone" -import { - FileList, - FileListAction, - FileListHeader, - FileListIcon, - FileListInfo, - FileListItem, - FileListName, - FileListSize, -} from "@/components/ui/file-list" -import { Badge } from "@/components/ui/badge" -import { ScrollArea } from "@/components/ui/scroll-area" -import { Upload, X, Loader2 } from "lucide-react" -import prettyBytes from "pretty-bytes" -import { EnhancedDocumentsView } from "@/db/schema/vendorDocu" - -// 리비전 업로드 스키마 -const revisionUploadSchema = z.object({ - stage: z.string().min(1, "스테이지는 필수입니다"), - revision: z.string().min(1, "리비전은 필수입니다"), - uploaderName: z.string().optional(), - comment: z.string().optional(), - attachments: z.array(z.instanceof(File)).min(1, "최소 1개 파일이 필요합니다"), - // ✅ B3 문서용 usage 필드 추가 - usage: z.string().optional(), -}).refine((data) => { - // B3 문서이고 특정 stage인 경우 usage 필수 - // 이 검증은 컴포넌트 내에서 조건부로 처리 - return true; -}, { - message: "Usage는 필수입니다", - path: ["usage"], -}); - -const getUsageOptions = (stageName: string): string[] => { - const stageNameLower = stageName.toLowerCase(); - - if (stageNameLower.includes('approval')) { - return ['Approval (Partial)', 'Approval (Full)']; - } else if (stageNameLower.includes('working')) { - return ['Working (Partial)', 'Working (Full)']; - } - - return []; -}; - - -type RevisionUploadSchema = z.infer<typeof revisionUploadSchema> - -interface RevisionUploadDialogProps { - open: boolean - onOpenChange: (open: boolean) => void - document: EnhancedDocumentsView | null - projectType: "ship" | "plant" - presetStage?: string - presetRevision?: string - mode?: 'new' | 'append' - onUploadComplete?: () => void // ✅ 업로드 완료 콜백 추가 -} - -function getTargetSystem(projectType: "ship" | "plant") { - return projectType === "ship" ? "DOLCE" : "SWP" -} - -export function RevisionUploadDialog({ - open, - onOpenChange, - document, - projectType, - presetStage, - presetRevision, - mode = 'new', - onUploadComplete, -}: RevisionUploadDialogProps) { - - const targetSystem = React.useMemo( - () => getTargetSystem(projectType), - [projectType] - ) - - const [selectedFiles, setSelectedFiles] = React.useState<File[]>([]) - const [isUploading, setIsUploading] = React.useState(false) - const [uploadProgress, setUploadProgress] = React.useState(0) - const router = useRouter() - - const { data: session } = useSession() - - // 사용 가능한 스테이지 옵션 - const stageOptions = React.useMemo(() => { - if (document?.allStages) { - return document.allStages.map(stage => stage.stageName) - } - return ["Issued for Review", "AFC", "Final Issue"] - }, [document]) - - const form = useForm<RevisionUploadSchema>({ - resolver: zodResolver(revisionUploadSchema), - defaultValues: { - stage: presetStage || document?.currentStageName || "", - revision: presetRevision || "", - uploaderName: session?.user?.name || "", - comment: "", - attachments: [], - usage: "", // ✅ usage 기본값 추가 - }, - }) - - // ✅ 현재 선택된 stage 값을 watch - const currentStage = form.watch('stage') - - // ✅ B3 문서 여부 확인 - const isB3Document = document?.drawingKind === 'B3' - - // ✅ 현재 stage에 따른 usage 옵션 - const usageOptions = React.useMemo(() => { - if (!isB3Document || !currentStage) return [] - return getUsageOptions(currentStage) - }, [isB3Document, currentStage]) - - // ✅ usage 필드가 필요한지 확인 - const isUsageRequired = isB3Document && usageOptions.length > 0 - - // session이 로드되면 uploaderName 업데이트 - React.useEffect(() => { - if (session?.user?.name) { - form.setValue('uploaderName', session.user.name) - } - }, [session?.user?.name, form]) - - // presetStage와 presetRevision이 변경될 때 폼 값 업데이트 - React.useEffect(() => { - if (presetStage) { - form.setValue('stage', presetStage) - } - if (presetRevision) { - form.setValue('revision', presetRevision) - } - }, [presetStage, presetRevision, form]) - - // ✅ stage가 변경될 때 usage 값 리셋 - React.useEffect(() => { - if (isB3Document) { - const newUsageOptions = getUsageOptions(currentStage) - if (newUsageOptions.length === 0) { - form.setValue('usage', '') - } else { - // 기존 값이 새로운 옵션에 없으면 리셋 - const currentUsage = form.getValues('usage') - if (currentUsage && !newUsageOptions.includes(currentUsage)) { - form.setValue('usage', '') - } - } - } - }, [currentStage, isB3Document, form]) - - // 파일 드롭 처리 - const handleDropAccepted = (acceptedFiles: File[]) => { - const newFiles = [...selectedFiles, ...acceptedFiles] - setSelectedFiles(newFiles) - form.setValue('attachments', newFiles, { shouldValidate: true }) - } - - const removeFile = (index: number) => { - const updatedFiles = [...selectedFiles] - updatedFiles.splice(index, 1) - setSelectedFiles(updatedFiles) - form.setValue('attachments', updatedFiles, { shouldValidate: true }) - } - - // 캐시 갱신 함수 - const refreshCaches = async () => { - try { - router.refresh() - - if (document?.contractId) { - await mutate(`/api/sync/status/${document.contractId}/${targetSystem}`) - console.log('✅ Sync status cache refreshed') - } - - await mutate(key => - typeof key === 'string' && - key.includes('sync') && - key.includes(String(document?.contractId)) - ) - - onUploadComplete?.() - - console.log('✅ All caches refreshed after upload') - } catch (error) { - console.error('❌ Cache refresh failed:', error) - } - } - - // ✅ 업로드 처리 - usage 필드 검증 및 전송 - async function onSubmit(data: RevisionUploadSchema) { - if (!document) return - - // ✅ B3 문서에서 usage가 필요한 경우 검증 - if (isUsageRequired && !data.usage) { - form.setError('usage', { - type: 'required', - message: 'Usage 선택은 필수입니다' - }) - return - } - - setIsUploading(true) - setUploadProgress(0) - - try { - const formData = new FormData() - formData.append("documentId", String(document.documentId)) - formData.append("stage", data.stage) - formData.append("revision", data.revision) - formData.append("mode", mode) - formData.append("targetSystem", targetSystem) - - if (data.uploaderName) { - formData.append("uploaderName", data.uploaderName) - } - - if (data.comment) { - formData.append("comment", data.comment) - } - - // ✅ B3 문서인 경우 usage 추가 - if (isB3Document && data.usage) { - formData.append("usage", data.usage) - } - - // 파일들 추가 - data.attachments.forEach((file) => { - formData.append("attachments", file) - }) - - // 진행률 업데이트 시뮬레이션 - const updateProgress = (progress: number) => { - setUploadProgress(Math.min(progress, 95)) - } - - const totalSize = data.attachments.reduce((sum, file) => sum + file.size, 0) - let uploadedSize = 0 - - const progressInterval = setInterval(() => { - uploadedSize += totalSize * 0.1 - const progress = Math.min((uploadedSize / totalSize) * 100, 90) - updateProgress(progress) - }, 300) - - const response = await fetch('/api/revision-upload', { - method: 'POST', - body: formData, - }) - - clearInterval(progressInterval) - - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.error || errorData.details || '업로드에 실패했습니다.') - } - - const result = await response.json() - setUploadProgress(100) - - toast.success( - result.message || - `리비전 ${data.revision}이 성공적으로 업로드되었습니다. (${result.data?.uploadedFiles?.length || 0}개 파일)` - ) - - console.log('✅ 업로드 성공:', result) - - setTimeout(async () => { - await refreshCaches() - handleDialogClose() - }, 1000) - - } catch (error) { - console.error('❌ 업로드 오류:', error) - toast.error(error instanceof Error ? error.message : "업로드 중 오류가 발생했습니다") - } finally { - setIsUploading(false) - setTimeout(() => setUploadProgress(0), 2000) - } - } - - const handleDialogClose = () => { - form.reset({ - stage: presetStage || document?.currentStageName || "", - revision: presetRevision || "", - uploaderName: session?.user?.name || "", - comment: "", - attachments: [], - usage: "", // ✅ usage 리셋 추가 - }) - setSelectedFiles([]) - setIsUploading(false) - setUploadProgress(0) - onOpenChange(false) - } - - return ( - <Dialog open={open} onOpenChange={handleDialogClose}> - <DialogContent className="sm:max-w-md"> - <DialogHeader> - <DialogTitle className="flex items-center gap-2"> - <Upload className="w-5 h-5" /> - {mode === 'new' ? '새 리비전 업로드' : '파일 추가'} - </DialogTitle> - <DialogDescription> - {document ? `${document.docNumber} - ${document.title}` : - mode === 'new' ? "문서에 새 리비전을 업로드합니다." : "기존 리비전에 파일을 추가합니다."} - </DialogDescription> - - <div className="flex items-center gap-2 pt-2 flex-wrap"> - <Badge variant={projectType === "ship" ? "default" : "secondary"}> - {projectType === "ship" ? "조선 프로젝트" : "플랜트 프로젝트"} - </Badge> - <Badge variant="outline" className="text-xs"> - → {targetSystem} - </Badge> - {/* ✅ B3 문서 표시 */} - {isB3Document && ( - <Badge variant="outline" className="text-xs bg-orange-50 text-orange-700 border-orange-200"> - B3 문서 - </Badge> - )} - {session?.user?.name && ( - <Badge variant="outline" className="text-xs"> - 업로더: {session.user.name} - </Badge> - )} - {mode === 'append' && presetRevision && ( - <Badge variant="outline" className="text-xs"> - 리비전 {presetRevision}에 파일 추가 - </Badge> - )} - {mode === 'new' && presetRevision && ( - <Badge variant="outline" className="text-xs"> - 다음 리비전: {presetRevision} - </Badge> - )} - </div> - </DialogHeader> - - <Form {...form}> - <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> - <div className="grid grid-cols-2 gap-4"> - <FormField - control={form.control} - name="stage" - render={({ field }) => ( - <FormItem> - <FormLabel>스테이지</FormLabel> - <Select onValueChange={field.onChange} value={field.value}> - <FormControl> - <SelectTrigger> - <SelectValue placeholder="스테이지 선택" /> - </SelectTrigger> - </FormControl> - <SelectContent> - {stageOptions.map((stage) => ( - <SelectItem key={stage} value={stage}> - {stage} - </SelectItem> - ))} - </SelectContent> - </Select> - <FormMessage /> - </FormItem> - )} - /> - - <FormField - control={form.control} - name="revision" - render={({ field }) => ( - <FormItem> - <FormLabel>리비전</FormLabel> - <FormControl> - <Input - {...field} - placeholder="예: A, B, 1, 2..." - readOnly={mode === 'append'} - className={mode === 'append' ? 'bg-gray-50' : ''} - /> - </FormControl> - <FormMessage /> - {mode === 'new' && presetRevision && ( - <p className="text-xs text-gray-500"> - 자동으로 계산된 다음 리비전입니다. - </p> - )} - {mode === 'append' && ( - <p className="text-xs text-gray-500"> - 기존 리비전에 파일을 추가합니다. - </p> - )} - </FormItem> - )} - /> - </div> - - {/* ✅ B3 문서용 Usage 필드 - 조건부 표시 */} - {isB3Document && usageOptions.length > 0 && ( - <FormField - control={form.control} - name="usage" - render={({ field }) => ( - <FormItem> - <FormLabel className="flex items-center gap-2"> - 용도 - {isUsageRequired && <span className="text-red-500">*</span>} - </FormLabel> - <Select onValueChange={field.onChange} value={field.value}> - <FormControl> - <SelectTrigger> - <SelectValue placeholder="용도를 선택하세요" /> - </SelectTrigger> - </FormControl> - <SelectContent> - {usageOptions.map((usage) => ( - <SelectItem key={usage} value={usage}> - {usage} - </SelectItem> - ))} - </SelectContent> - </Select> - <FormMessage /> - <p className="text-xs text-gray-500"> - {currentStage} 스테이지에 필요한 용도를 선택하세요. - </p> - </FormItem> - )} - /> - )} - - <FormField - control={form.control} - name="uploaderName" - render={({ field }) => ( - <FormItem> - <FormLabel>업로더명</FormLabel> - <FormControl> - <Input - {...field} - placeholder="업로더 이름을 입력하세요" - className="bg-gray-50" - /> - </FormControl> - <FormMessage /> - <p className="text-xs text-gray-500"> - 로그인된 사용자 정보가 자동으로 입력됩니다. - </p> - </FormItem> - )} - /> - - <FormField - control={form.control} - name="comment" - render={({ field }) => ( - <FormItem> - <FormLabel>코멘트 (선택)</FormLabel> - <FormControl> - <Textarea {...field} placeholder="코멘트를 입력하세요" rows={3} /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - - {/* 파일 업로드 영역 */} - <FormField - control={form.control} - name="attachments" - render={() => ( - <FormItem> - <FormLabel>파일 첨부</FormLabel> - <Dropzone - maxSize={3e9} // 3GB - multiple={true} - onDropAccepted={handleDropAccepted} - disabled={isUploading} - > - <DropzoneZone className="flex justify-center"> - <FormControl> - <DropzoneInput /> - </FormControl> - <div className="flex items-center gap-6"> - <DropzoneUploadIcon /> - <div className="grid gap-0.5"> - <DropzoneTitle>파일을 여기에 드롭하세요</DropzoneTitle> - <DropzoneDescription> - 또는 클릭하여 파일을 선택하세요 - </DropzoneDescription> - </div> - </div> - </DropzoneZone> - </Dropzone> - <FormMessage /> - </FormItem> - )} - /> - - {/* 선택된 파일 목록 */} - {selectedFiles.length > 0 && ( - <div className="space-y-2"> - <div className="flex items-center justify-between"> - <h6 className="text-sm font-semibold"> - 선택된 파일 ({selectedFiles.length}) - </h6> - </div> - <ScrollArea className="max-h-[200px]"> - <FileList> - {selectedFiles.map((file, index) => ( - <FileListItem key={index} className="p-3"> - <FileListHeader> - <FileListIcon /> - <FileListInfo> - <FileListName>{file.name}</FileListName> - <FileListSize>{file.size}</FileListSize> - </FileListInfo> - <FileListAction - onClick={() => removeFile(index)} - disabled={isUploading} - > - <X className="h-4 w-4" /> - </FileListAction> - </FileListHeader> - </FileListItem> - ))} - </FileList> - </ScrollArea> - </div> - )} - - {/* 업로드 진행 상태 */} - {isUploading && ( - <div className="space-y-2"> - <div className="flex items-center gap-2"> - <Loader2 className="h-4 w-4 animate-spin" /> - <span className="text-sm">{uploadProgress}% 업로드 중...</span> - </div> - <div className="h-2 w-full bg-muted rounded-full overflow-hidden"> - <div - className="h-full bg-primary rounded-full transition-all" - style={{ width: `${uploadProgress}%` }} - /> - </div> - </div> - )} - - <DialogFooter> - <Button - type="button" - variant="outline" - onClick={handleDialogClose} - disabled={isUploading} - > - 취소 - </Button> - <Button - type="submit" - disabled={isUploading || selectedFiles.length === 0} - > - {isUploading ? ( - <> - <Loader2 className="mr-2 h-4 w-4 animate-spin" /> - 업로드 중... - </> - ) : ( - <> - <Upload className="mr-2 h-4 w-4" /> - 업로드 - </> - )} - </Button> - </DialogFooter> - </form> - </Form> - </DialogContent> - </Dialog> - ) -}
\ No newline at end of file |
