// enhanced-document-sheet.tsx "use client" import * as React from "react" import { zodResolver } from "@hookform/resolvers/zod" import { useForm } from "react-hook-form" import { toast } from "sonner" import { z } from "zod" import { useRouter } from "next/navigation" import { Loader, Save, Upload, Calendar, User, FileText, AlertTriangle, CheckCircle, Clock, Plus, X } from "lucide-react" import { Button } from "@/components/ui/button" import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form" import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, } from "@/components/ui/sheet" import { Tabs, TabsContent, TabsList, TabsTrigger, } from "@/components/ui/tabs" import { Input } from "@/components/ui/input" import { Textarea } from "@/components/ui/textarea" import { Badge } from "@/components/ui/badge" import { Separator } from "@/components/ui/separator" import { ScrollArea } from "@/components/ui/scroll-area" import { Calendar as CalendarComponent } from "@/components/ui/calendar" import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" import { cn } from "@/lib/utils" import { format } from "date-fns" import { ko } from "date-fns/locale" import { EnhancedDocumentsView } from "@/db/schema/vendorDocu" // 드롭존과 파일 관련 컴포넌트들 import { Dropzone, DropzoneDescription, DropzoneInput, DropzoneTitle, DropzoneUploadIcon, DropzoneZone, } from "@/components/ui/dropzone" import { FileList, FileListAction, FileListDescription, FileListHeader, FileListIcon, FileListInfo, FileListItem, FileListName, FileListSize, } from "@/components/ui/file-list" import prettyBytes from "pretty-bytes" // 스키마 정의 const enhancedDocumentSchema = z.object({ // 기본 문서 정보 docNumber: z.string().min(1, "문서번호는 필수입니다"), title: z.string().min(1, "제목은 필수입니다"), pic: z.string().optional(), status: z.string().min(1, "상태는 필수입니다"), issuedDate: z.date().optional(), // 스테이지 관리 (plant 타입에서만 수정 가능) stages: z.array(z.object({ id: z.number().optional(), stageName: z.string().min(1, "스테이지명은 필수입니다"), stageOrder: z.number(), priority: z.enum(["HIGH", "MEDIUM", "LOW"]).default("MEDIUM"), planDate: z.date().optional(), assigneeName: z.string().optional(), description: z.string().optional(), })).optional(), // 리비전 업로드 (현재 스테이지에 대한) newRevision: z.object({ stage: z.string().optional(), revision: z.string().optional(), uploaderType: z.enum(["vendor", "client", "shi"]).default("vendor"), uploaderName: z.string().optional(), comment: z.string().optional(), attachments: z.array(z.instanceof(File)).optional(), }).optional(), }) type EnhancedDocumentSchema = z.infer // 상태 옵션 정의 const statusOptions = [ { value: "ACTIVE", label: "활성" }, { value: "INACTIVE", label: "비활성" }, { value: "COMPLETED", label: "완료" }, { value: "CANCELLED", label: "취소" }, ] const priorityOptions = [ { value: "HIGH", label: "높음" }, { value: "MEDIUM", label: "보통" }, { value: "LOW", label: "낮음" }, ] const stageStatusOptions = [ { value: "PLANNED", label: "계획됨" }, { value: "IN_PROGRESS", label: "진행중" }, { value: "SUBMITTED", label: "제출됨" }, { value: "APPROVED", label: "승인됨" }, { value: "REJECTED", label: "반려됨" }, { value: "COMPLETED", label: "완료됨" }, ] interface EnhancedDocumentSheetProps extends React.ComponentPropsWithRef { document: EnhancedDocumentsView | null projectType: "ship" | "plant" mode: "view" | "edit" | "upload" | "schedule" | "approve" } export function EnhancedDocumentSheet({ document, projectType, mode = "view", ...props }: EnhancedDocumentSheetProps) { const [isUpdatePending, startUpdateTransition] = React.useTransition() const [selectedFiles, setSelectedFiles] = React.useState([]) const [uploadProgress, setUploadProgress] = React.useState(0) const [activeTab, setActiveTab] = React.useState("info") const router = useRouter() // 권한 계산 const permissions = React.useMemo(() => { const canEdit = projectType === "plant" || mode === "edit" const canUpload = mode === "upload" || mode === "edit" const canApprove = mode === "approve" && projectType === "ship" const canSchedule = mode === "schedule" || (projectType === "plant" && mode === "edit") return { canEdit, canUpload, canApprove, canSchedule } }, [projectType, mode]) const form = useForm({ resolver: zodResolver(enhancedDocumentSchema), defaultValues: { docNumber: "", title: "", pic: "", status: "ACTIVE", issuedDate: undefined, stages: [], newRevision: { stage: "", revision: "", uploaderType: "vendor", uploaderName: "", comment: "", attachments: [], }, }, }) // 폼 초기화 React.useEffect(() => { if (document) { form.reset({ docNumber: document.docNumber, title: document.title, pic: document.pic || "", status: document.status, issuedDate: document.issuedDate ? new Date(document.issuedDate) : undefined, stages: document.allStages?.map((stage, index) => ({ id: stage.id, stageName: stage.stageName, stageOrder: stage.stageOrder || index, priority: stage.priority as "HIGH" | "MEDIUM" | "LOW" || "MEDIUM", planDate: stage.planDate ? new Date(stage.planDate) : undefined, assigneeName: stage.assigneeName || "", description: "", })) || [], newRevision: { stage: document.currentStageName || "", revision: "", uploaderType: "vendor", uploaderName: "", comment: "", attachments: [], }, }) // 모드에 따른 기본 탭 설정 if (mode === "upload") { setActiveTab("upload") } else if (mode === "schedule") { setActiveTab("schedule") } else if (mode === "approve") { setActiveTab("approve") } } }, [document, form, mode]) // 파일 처리 const handleDropAccepted = (acceptedFiles: File[]) => { const newFiles = [...selectedFiles, ...acceptedFiles] setSelectedFiles(newFiles) form.setValue('newRevision.attachments', newFiles) } const removeFile = (index: number) => { const updatedFiles = [...selectedFiles] updatedFiles.splice(index, 1) setSelectedFiles(updatedFiles) form.setValue('newRevision.attachments', updatedFiles) } // 스테이지 추가/제거 const addStage = () => { const currentStages = form.getValues("stages") || [] const newStage = { stageName: "", stageOrder: currentStages.length, priority: "MEDIUM" as const, planDate: undefined, assigneeName: "", description: "", } form.setValue("stages", [...currentStages, newStage]) } const removeStage = (index: number) => { const currentStages = form.getValues("stages") || [] const updatedStages = currentStages.filter((_, i) => i !== index) form.setValue("stages", updatedStages) } // 제출 처리 function onSubmit(input: EnhancedDocumentSchema) { startUpdateTransition(async () => { if (!document) return try { // 모드에 따른 다른 처리 switch (mode) { case "edit": // 문서 정보 업데이트 + 스테이지 관리 await updateDocumentInfo(input) break case "upload": // 리비전 업로드 await uploadRevision(input) break case "approve": // 승인 처리 await approveRevision(input) break case "schedule": // 스케줄 관리 await updateSchedule(input) break } form.reset() setSelectedFiles([]) props.onOpenChange?.(false) toast.success("성공적으로 처리되었습니다") router.refresh() } catch (error) { toast.error("처리 중 오류가 발생했습니다") console.error(error) } }) } // 개별 처리 함수들 const updateDocumentInfo = async (input: EnhancedDocumentSchema) => { // 문서 기본 정보 업데이트 API 호출 console.log("문서 정보 업데이트:", input) } const uploadRevision = async (input: EnhancedDocumentSchema) => { if (!input.newRevision?.attachments?.length) { throw new Error("파일을 선택해주세요") } // 파일 업로드 처리 const formData = new FormData() formData.append("documentId", String(document?.documentId)) formData.append("stage", input.newRevision.stage || "") formData.append("revision", input.newRevision.revision || "") formData.append("uploaderType", input.newRevision.uploaderType) input.newRevision.attachments.forEach((file) => { formData.append("attachments", file) }) // API 호출 console.log("리비전 업로드:", formData) } const approveRevision = async (input: EnhancedDocumentSchema) => { // 승인 처리 API 호출 console.log("리비전 승인:", input) } const updateSchedule = async (input: EnhancedDocumentSchema) => { // 스케줄 업데이트 API 호출 console.log("스케줄 업데이트:", input) } // 제목 및 설명 생성 const getSheetTitle = () => { switch (mode) { case "edit": return "문서 정보 수정" case "upload": return "리비전 업로드" case "approve": return "문서 승인" case "schedule": return "일정 관리" default: return "문서 상세" } } const getSheetDescription = () => { const docInfo = document ? `${document.docNumber} - ${document.title}` : "" switch (mode) { case "edit": return `문서 정보를 수정합니다. ${docInfo}` case "upload": return `새 리비전을 업로드합니다. ${docInfo}` case "approve": return `문서를 검토하고 승인 처리합니다. ${docInfo}` case "schedule": return `문서의 일정을 관리합니다. ${docInfo}` default: return docInfo } } return ( {mode === "upload" && } {mode === "approve" && } {mode === "schedule" && } {mode === "edit" && } {getSheetTitle()} {getSheetDescription()} {/* 프로젝트 타입 및 권한 표시 */}
{projectType === "ship" ? "조선 프로젝트" : "플랜트 프로젝트"} {document?.isOverdue && ( 지연 )} {document?.currentStagePriority === "HIGH" && ( 높은 우선순위 )}
기본정보 일정관리 리비전업로드 승인처리 {/* 기본 정보 탭 */}
( 문서번호 )} /> ( 제목 )} />
( 담당자 (PIC) )} /> ( 상태 )} />
( 발행일 date > new Date()} initialFocus /> )} /> {/* 현재 상태 정보 표시 */} {document && (

현재 진행 상황

현재 스테이지:

{document.currentStageName || "-"}

진행률:

{document.progressPercentage || 0}%

최신 리비전:

{document.latestRevision || "-"}

담당자:

{document.currentStageAssigneeName || "-"}

)}
{/* 일정 관리 탭 */}

스테이지 일정 관리

{projectType === "plant" && ( )}
{form.watch("stages")?.map((stage, index) => (
스테이지 {index + 1}
{projectType === "plant" && ( )}
( 스테이지명 )} /> ( 우선순위 )} /> ( 계획일 )} /> ( 담당자 )} />
))}
{/* 리비전 업로드 탭 */}
( 스테이지 )} /> ( 리비전 )} />
( 업로더명 (선택) )} /> ( 코멘트 (선택)