summaryrefslogtreecommitdiff
path: root/lib/vendor-document-list/table/revision-upload-dialog.tsx
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-05-28 00:32:31 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-05-28 00:32:31 +0000
commit20800b214145ee6056f94ca18fa1054f145eb977 (patch)
treeb5c8b27febe5b126e6d9ece115ea05eace33a020 /lib/vendor-document-list/table/revision-upload-dialog.tsx
parente1344a5da1aeef8fbf0f33e1dfd553078c064ccc (diff)
(대표님) lib 파트 커밋
Diffstat (limited to 'lib/vendor-document-list/table/revision-upload-dialog.tsx')
-rw-r--r--lib/vendor-document-list/table/revision-upload-dialog.tsx486
1 files changed, 486 insertions, 0 deletions
diff --git a/lib/vendor-document-list/table/revision-upload-dialog.tsx b/lib/vendor-document-list/table/revision-upload-dialog.tsx
new file mode 100644
index 00000000..ac58b974
--- /dev/null
+++ b/lib/vendor-document-list/table/revision-upload-dialog.tsx
@@ -0,0 +1,486 @@
+"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 {
+ 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개 파일이 필요합니다"),
+})
+
+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'
+}
+
+export function RevisionUploadDialog({
+ open,
+ onOpenChange,
+ document,
+ projectType,
+ presetStage,
+ presetRevision,
+ mode = 'new',
+}: RevisionUploadDialogProps) {
+ const [selectedFiles, setSelectedFiles] = React.useState<File[]>([])
+ const [isUploading, setIsUploading] = React.useState(false)
+ const [uploadProgress, setUploadProgress] = React.useState(0)
+ const router = useRouter()
+
+ // ✅ next-auth session 가져오기
+ 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 || "", // ✅ session.user.name 사용
+ comment: "",
+ attachments: [],
+ },
+ })
+
+ // ✅ 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])
+
+ // 파일 드롭 처리
+ 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 })
+ }
+
+ // 업로드 처리
+ async function onSubmit(data: RevisionUploadSchema) {
+ if (!document) 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) // 'new' 또는 'append'
+
+ if (data.uploaderName) {
+ formData.append("uploaderName", data.uploaderName)
+ }
+
+ if (data.comment) {
+ formData.append("comment", data.comment)
+ }
+
+ // 파일들 추가
+ data.attachments.forEach((file) => {
+ formData.append("attachments", file)
+ })
+
+ // 진행률 업데이트 시뮬레이션
+ const updateProgress = (progress: number) => {
+ setUploadProgress(Math.min(progress, 95)) // 95%까지만 진행률 표시
+ }
+
+ // 파일 크기에 따른 진행률 시뮬레이션
+ const totalSize = data.attachments.reduce((sum, file) => sum + file.size, 0)
+ let uploadedSize = 0
+
+ const progressInterval = setInterval(() => {
+ uploadedSize += totalSize * 0.1 // 10%씩 증가 시뮬레이션
+ const progress = Math.min((uploadedSize / totalSize) * 100, 90)
+ updateProgress(progress)
+ }, 300)
+
+ // ✅ 실제 API 호출
+ 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(() => {
+ handleDialogClose()
+ router.refresh()
+ }, 1000)
+
+ } catch (error) {
+ console.error('❌ 업로드 오류:', error)
+ toast.error(error instanceof Error ? error.message : "업로드 중 오류가 발생했습니다")
+ } finally {
+ setIsUploading(false)
+ setTimeout(() => setUploadProgress(0), 2000) // 2초 후 진행률 리셋
+ }
+ }
+
+ const handleDialogClose = () => {
+ form.reset({
+ stage: presetStage || document?.currentStageName || "",
+ revision: presetRevision || "",
+ uploaderName: session?.user?.name || "", // ✅ 다이얼로그 닫을 때도 session 값으로 리셋
+ comment: "",
+ attachments: [],
+ })
+ 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">
+ <Badge variant={projectType === "ship" ? "default" : "secondary"}>
+ {projectType === "ship" ? "조선 프로젝트" : "플랜트 프로젝트"}
+ </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>
+
+ <FormField
+ control={form.control}
+ name="uploaderName"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>업로더명</FormLabel>
+ <FormControl>
+ <Input
+ {...field}
+ placeholder="업로더 이름을 입력하세요"
+ className="bg-gray-50" // ✅ session 값이므로 읽기 전용 느낌으로 스타일링
+ />
+ </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>{prettyBytes(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