summaryrefslogtreecommitdiff
path: root/components/ship-vendor-document/new-revision-dialog.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'components/ship-vendor-document/new-revision-dialog.tsx')
-rw-r--r--components/ship-vendor-document/new-revision-dialog.tsx622
1 files changed, 622 insertions, 0 deletions
diff --git a/components/ship-vendor-document/new-revision-dialog.tsx b/components/ship-vendor-document/new-revision-dialog.tsx
new file mode 100644
index 00000000..092256c7
--- /dev/null
+++ b/components/ship-vendor-document/new-revision-dialog.tsx
@@ -0,0 +1,622 @@
+"use client"
+
+import React from "react"
+import { useForm } from "react-hook-form"
+import { zodResolver } from "@hookform/resolvers/zod"
+import { z } from "zod"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,DialogFooter
+} from "@/components/ui/dialog"
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form"
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select"
+import { Input } from "@/components/ui/input"
+import { Textarea } from "@/components/ui/textarea"
+import { Button } from "@/components/ui/button"
+import { Progress } from "@/components/ui/progress"
+import { Badge } from "@/components/ui/badge"
+import {
+ Upload,
+ FileText,
+ X,
+ Loader2,
+ AlertCircle,
+ CheckCircle
+} from "lucide-react"
+import { toast } from "sonner"
+import { useSession } from "next-auth/react"
+
+// 기존 메인 컴포넌트에서 추가할 import
+// import { NewRevisionDialog } from "./new-revision-dialog"
+
+/* -------------------------------------------------------------------------------------------------
+ * Schema & Types
+ * -----------------------------------------------------------------------------------------------*/
+
+// 파일 검증 스키마
+const MAX_FILE_SIZE = 50 * 1024 * 1024 // 50MB
+const ACCEPTED_FILE_TYPES = [
+ 'application/pdf',
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+ 'application/vnd.ms-excel',
+ 'image/jpeg',
+ 'image/png',
+ 'image/gif',
+ 'text/plain',
+ 'application/zip',
+ 'application/x-zip-compressed'
+]
+
+// drawingKind에 따른 동적 스키마 생성
+const createRevisionUploadSchema = (drawingKind: string) => {
+ const baseSchema = {
+ usage: z.string().min(1, "용도를 선택해주세요"),
+ revision: z.string().min(1, "리비전을 입력해주세요").max(50, "리비전은 50자 이내로 입력해주세요"),
+ comment: z.string().optional(),
+ attachments: z
+ .array(z.instanceof(File))
+ .min(1, "최소 1개의 파일을 업로드해주세요")
+ .max(10, "최대 10개의 파일까지 업로드 가능합니다")
+ .refine(
+ (files) => files.every((file) => file.size <= MAX_FILE_SIZE),
+ "파일 크기는 50MB 이하여야 합니다"
+ )
+ .refine(
+ (files) => files.every((file) => ACCEPTED_FILE_TYPES.includes(file.type)),
+ "지원하지 않는 파일 형식입니다"
+ ),
+ }
+
+ // B3인 경우에만 usageType 필드 추가
+ if (drawingKind === 'B3') {
+ return z.object({
+ ...baseSchema,
+ usageType: z.string().min(1, "용도 타입을 선택해주세요"),
+ })
+ } else {
+ return z.object({
+ ...baseSchema,
+ usageType: z.string().optional(),
+ })
+ }
+}
+
+// drawingKind에 따른 용도 옵션
+const getUsageOptions = (drawingKind: string) => {
+ switch (drawingKind) {
+ case 'B3':
+ return [
+ { value: "Approval", label: "Approval" },
+ { value: "Working", label: "Working" },
+ { value: "Reference", label: "Reference" },
+ ]
+ case 'B4':
+ return [
+ { value: "Pre", label: "Pre" },
+ { value: "Working", label: "Working" },
+ ]
+ case 'B5':
+ return [
+ { value: "Pre", label: "Pre" },
+ { value: "Working", label: "Working" },
+ ]
+ default:
+ return [
+ { value: "Pre", label: "Pre" },
+ { value: "Working", label: "Working" },
+ ]
+ }
+}
+
+// B3 전용 용도 타입 옵션
+const getUsageTypeOptions = (usage: string) => {
+ switch (usage) {
+ case 'Approval':
+ return [
+ { value: "Approval Submission Full", label: "Approval Submission Full" },
+ { value: "Approval Submission Partial", label: "Approval Submission Partial" },
+ { value: "Approval Completion Full", label: "Approval Completion Full" },
+ { value: "Approval Completion Partial", label: "Approval Completion Partial" },
+ ]
+ case 'Working':
+ return [
+ { value: "Working Full", label: "Working Full" },
+ { value: "Working Partial", label: "Working Partial" },
+ ]
+ case 'Reference':
+ return [
+ { value: "Reference Full", label: "Reference Full" },
+ { value: "Reference Partial", label: "Reference Partial" },
+ { value: "Reference Series Full", label: "Reference Series Full" },
+ { value: "Reference Series Partial", label: "Reference Series Partial" },
+ ]
+ default:
+ return []
+ }
+}
+
+// 리비전 형식 가이드 생성
+const getRevisionGuide = (drawingKind: string, usage: string, usageType: string) => {
+ if (drawingKind === 'B4') {
+ return "R00, R01, R02... 형식으로 입력하세요"
+ }
+
+ if (drawingKind === 'B5') {
+ return "A, B, C... 형식으로 입력하세요"
+ }
+
+ if (drawingKind === 'B3') {
+ if (usage === 'Reference') {
+ return "00, 01, 02... 형식으로 입력하세요"
+ }
+ if (usageType === 'Working Partial') {
+ return "00, 01, 02... 형식으로 입력하세요"
+ }
+ if (usageType?.includes('Full') && (usageType.includes('Working') || usageType.includes('Approval'))) {
+ return "A, B, C... 형식으로 입력하세요"
+ }
+ if (usageType?.includes('Partial') && usageType.includes('Approval')) {
+ return "리비전 형식은 추후 정의 예정입니다"
+ }
+ }
+
+ return "리비전을 입력하세요"
+}
+
+interface NewRevisionDialogProps {
+ open: boolean
+ onOpenChange: (open: boolean) => void
+ documentId: number
+ documentTitle?: string
+ drawingKind: string
+ onSuccess?: (result?: any) => void // ✅ result 파라미터 추가
+}
+
+/* -------------------------------------------------------------------------------------------------
+ * File Upload Component
+ * -----------------------------------------------------------------------------------------------*/
+function FileUploadArea({
+ files,
+ onFilesChange
+}: {
+ files: File[]
+ onFilesChange: (files: File[]) => void
+}) {
+ const fileInputRef = React.useRef<HTMLInputElement>(null)
+
+ const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
+ const selectedFiles = Array.from(event.target.files || [])
+ if (selectedFiles.length > 0) {
+ onFilesChange([...files, ...selectedFiles])
+ }
+ }
+
+ const handleDrop = (event: React.DragEvent<HTMLDivElement>) => {
+ event.preventDefault()
+ const droppedFiles = Array.from(event.dataTransfer.files)
+ if (droppedFiles.length > 0) {
+ onFilesChange([...files, ...droppedFiles])
+ }
+ }
+
+ const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => {
+ event.preventDefault()
+ }
+
+ const removeFile = (index: number) => {
+ onFilesChange(files.filter((_, i) => i !== index))
+ }
+
+ const formatFileSize = (bytes: number) => {
+ if (bytes === 0) return '0 Bytes'
+ const k = 1024
+ const sizes = ['Bytes', 'KB', 'MB', 'GB']
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
+ }
+
+ return (
+ <div className="space-y-4">
+ <div
+ className="border-2 border-dashed border-gray-300 rounded-lg p-6 text-center cursor-pointer hover:border-gray-400 transition-colors"
+ onDrop={handleDrop}
+ onDragOver={handleDragOver}
+ onClick={() => fileInputRef.current?.click()}
+ >
+ <Upload className="mx-auto h-12 w-12 text-gray-400 mb-4" />
+ <p className="text-sm text-gray-600 mb-2">
+ 파일을 드래그하여 놓거나 클릭하여 선택하세요
+ </p>
+ <p className="text-xs text-gray-500">
+ PDF, Word, Excel, 이미지, 텍스트, ZIP 파일 지원 (최대 50MB)
+ </p>
+ <input
+ ref={fileInputRef}
+ type="file"
+ multiple
+ accept=".pdf,.doc,.docx,.xls,.xlsx,.jpg,.jpeg,.png,.gif,.txt,.zip"
+ onChange={handleFileSelect}
+ className="hidden"
+ />
+ </div>
+
+ {files.length > 0 && (
+ <div className="space-y-2 max-h-40 overflow-y-auto overscroll-contain pr-2">
+ <p className="text-sm font-medium">선택된 파일 ({files.length}개)</p>
+ <div className="max-h-40 overflow-y-auto space-y-2">
+ {files.map((file, index) => (
+ <div
+ key={index}
+ className="flex items-center justify-between p-2 bg-gray-50 rounded border"
+ >
+ <div className="flex items-center space-x-2 flex-1">
+ <FileText className="h-4 w-4 text-gray-500" />
+ <div className="flex-1 min-w-0">
+ <p className="text-sm font-medium truncate" title={file.name}>
+ {file.name}
+ </p>
+ <p className="text-xs text-gray-500">
+ {formatFileSize(file.size)}
+ </p>
+ </div>
+ </div>
+ <Button
+ type="button"
+ variant="ghost"
+ size="sm"
+ onClick={() => removeFile(index)}
+ className="h-8 w-8 p-0"
+ >
+ <X className="h-4 w-4" />
+ </Button>
+ </div>
+ ))}
+ </div>
+ </div>
+ )}
+ </div>
+ )
+}
+
+/* -------------------------------------------------------------------------------------------------
+ * Main Dialog Component
+ * -----------------------------------------------------------------------------------------------*/
+export function NewRevisionDialog({
+ open,
+ onOpenChange,
+ documentId,
+ documentTitle,
+ drawingKind,
+ onSuccess
+}: NewRevisionDialogProps) {
+ const [isUploading, setIsUploading] = React.useState(false)
+ const [uploadProgress, setUploadProgress] = React.useState(0)
+ const { data: session } = useSession()
+
+ const userName = React.useMemo(() => {
+ return session?.user?.name ? session.user.name : null;
+ }, [session]);
+
+ // drawingKind에 따른 동적 스키마 및 옵션 생성
+ const revisionUploadSchema = React.useMemo(() => createRevisionUploadSchema(drawingKind), [drawingKind])
+ const usageOptions = React.useMemo(() => getUsageOptions(drawingKind), [drawingKind])
+ const showUsageType = drawingKind === 'B3'
+
+ type RevisionUploadSchema = z.infer<typeof revisionUploadSchema>
+
+ const form = useForm<RevisionUploadSchema>({
+ resolver: zodResolver(revisionUploadSchema),
+ defaultValues: {
+ usage: "",
+ revision: "",
+ comment: "",
+ usageType: showUsageType ? "" : undefined,
+ attachments: [],
+ },
+ })
+
+ const watchedFiles = form.watch("attachments")
+ const watchedUsage = form.watch("usage")
+ const watchedUsageType = form.watch("usageType")
+
+ // 용도 선택에 따른 용도 타입 옵션 업데이트
+ const usageTypeOptions = React.useMemo(() => {
+ if (drawingKind === 'B3' && watchedUsage) {
+ return getUsageTypeOptions(watchedUsage)
+ }
+ return []
+ }, [drawingKind, watchedUsage])
+
+ // 용도 변경 시 용도 타입 초기화
+ React.useEffect(() => {
+ if (showUsageType && watchedUsage) {
+ form.setValue("usageType", "")
+ }
+ }, [watchedUsage, showUsageType, form])
+
+ // 리비전 가이드 텍스트
+ const revisionGuide = React.useMemo(() => {
+ return getRevisionGuide(drawingKind, watchedUsage, watchedUsageType || "")
+ }, [drawingKind, watchedUsage, watchedUsageType])
+
+ const handleDialogClose = () => {
+ if (!isUploading) {
+ form.reset()
+ setUploadProgress(0)
+ onOpenChange(false)
+ }
+ }
+
+ const onSubmit = async (data: RevisionUploadSchema) => {
+ setIsUploading(true)
+ setUploadProgress(0)
+
+ try {
+ const formData = new FormData()
+ formData.append("documentId", String(documentId))
+ formData.append("usage", data.usage)
+ formData.append("revision", data.revision)
+ formData.append("uploaderName", userName || "evcp")
+
+ if (data.comment) {
+ formData.append("comment", data.comment)
+ }
+
+ // B3인 경우에만 usageType 추가
+ if (showUsageType && 'usageType' in data && data.usageType) {
+ formData.append("usageType", data.usageType)
+ }
+
+ // 파일들 추가
+ data.attachments.forEach((file) => {
+ formData.append("attachments", file)
+ })
+
+ // 진행률 업데이트 시뮬레이션
+ 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)
+ setUploadProgress(progress)
+ }, 300)
+
+ const response = await fetch('/api/revision-upload-ship', { // ✅ 올바른 API 엔드포인트 사용
+ 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()
+ onSuccess?.(result) // ✅ API 응답 결과를 콜백에 전달
+ }, 1000)
+
+ } catch (error) {
+ console.error('❌ 업로드 오류:', error)
+ toast.error(error instanceof Error ? error.message : "업로드 중 오류가 발생했습니다")
+ } finally {
+ setIsUploading(false)
+ setTimeout(() => setUploadProgress(0), 2000)
+ }
+ }
+
+ return (
+ <Dialog open={open} onOpenChange={handleDialogClose}>
+ <DialogContent className="max-w-2xl h-[90vh] flex flex-col overflow-hidden" style={{maxHeight:'90vh'}}>
+ {/* 고정 헤더 */}
+ <DialogHeader className="flex-shrink-0 pb-4 border-b">
+ <DialogTitle className="flex items-center gap-2">
+ <Upload className="h-5 w-5" />
+ 새 리비전 업로드
+ </DialogTitle>
+ {documentTitle && (
+ <DialogDescription className="text-sm space-y-1">
+ <div>문서: {documentTitle}</div>
+ </DialogDescription>
+ )}
+ </DialogHeader>
+
+ <Form {...form}>
+ <form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col flex-1 overflow-hidden">
+ {/* 스크롤 가능한 중간 영역 */}
+ <div className="flex-1 overflow-y-auto px-1 py-4 space-y-6">
+ {/* 용도 선택 */}
+ <FormField
+ control={form.control}
+ name="usage"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel className="required">용도</FormLabel>
+ <Select onValueChange={field.onChange} value={field.value}>
+ <FormControl>
+ <SelectTrigger>
+ <SelectValue placeholder="용도를 선택하세요" />
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ {usageOptions.map((option) => (
+ <SelectItem key={option.value} value={option.value}>
+ {option.label}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 용도 타입 선택 (B3만) */}
+ {showUsageType && watchedUsage && (
+ <FormField
+ control={form.control}
+ name="usageType"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel className="required">용도 타입</FormLabel>
+ <Select onValueChange={field.onChange} value={field.value}>
+ <FormControl>
+ <SelectTrigger>
+ <SelectValue placeholder="용도 타입을 선택하세요" />
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ {usageTypeOptions.map((option) => (
+ <SelectItem key={option.value} value={option.value}>
+ {option.label}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ )}
+
+ {/* 리비전 */}
+ <FormField
+ control={form.control}
+ name="revision"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel className="required">리비전</FormLabel>
+ <FormControl>
+ <Input
+ placeholder={revisionGuide}
+ {...field}
+ />
+ </FormControl>
+ <div className="text-xs text-muted-foreground mt-1">
+ {revisionGuide}
+ </div>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 코멘트 */}
+ <FormField
+ control={form.control}
+ name="comment"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>코멘트</FormLabel>
+ <FormControl>
+ <Textarea
+ placeholder="리비전에 대한 설명이나 변경사항을 입력하세요 (선택사항)"
+ className="resize-none"
+ rows={3}
+ {...field}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 파일 업로드 */}
+ <FormField
+ control={form.control}
+ name="attachments"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel className="required">첨부파일</FormLabel>
+ <FormControl>
+ <FileUploadArea
+ files={watchedFiles || []}
+ onFilesChange={field.onChange}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 업로드 진행률 */}
+ {isUploading && (
+ <div className="space-y-2">
+ <div className="flex items-center justify-between text-sm">
+ <span>업로드 진행률</span>
+ <span>{uploadProgress.toFixed(0)}%</span>
+ </div>
+ <Progress value={uploadProgress} className="w-full" />
+ {uploadProgress === 100 && (
+ <div className="flex items-center gap-2 text-sm text-green-600">
+ <CheckCircle className="h-4 w-4" />
+ <span>업로드 완료</span>
+ </div>
+ )}
+ </div>
+ )}
+ </div>
+
+ {/* 고정 버튼 영역 */}
+ <DialogFooter>
+ <Button
+ type="button"
+ variant="outline"
+ onClick={handleDialogClose}
+ disabled={isUploading}
+ >
+ 취소
+ </Button>
+ <Button
+ type="submit"
+ disabled={isUploading || !form.formState.isValid}
+ className="min-w-[120px]"
+ >
+ {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