summaryrefslogtreecommitdiff
path: root/lib/vendor-document-list/ship/revision-upload-dialog.tsx
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-06-13 07:11:18 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-06-13 07:11:18 +0000
commit0fddf148402fd6b99a1b3800d73679899bcb2ed3 (patch)
treeeb51c02e6fa6037ddcc38a3b57d10d8c739125cf /lib/vendor-document-list/ship/revision-upload-dialog.tsx
parentc72d0897f7b37843109c86f61d97eba05ba3ca0d (diff)
(대표님) 20250613 16시 10분 global css, b-rfq, document 등
Diffstat (limited to 'lib/vendor-document-list/ship/revision-upload-dialog.tsx')
-rw-r--r--lib/vendor-document-list/ship/revision-upload-dialog.tsx629
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