diff options
Diffstat (limited to 'lib/vendor-document-list/table/enhanced-document-sheet.tsx')
| -rw-r--r-- | lib/vendor-document-list/table/enhanced-document-sheet.tsx | 939 |
1 files changed, 939 insertions, 0 deletions
diff --git a/lib/vendor-document-list/table/enhanced-document-sheet.tsx b/lib/vendor-document-list/table/enhanced-document-sheet.tsx new file mode 100644 index 00000000..88e342c8 --- /dev/null +++ b/lib/vendor-document-list/table/enhanced-document-sheet.tsx @@ -0,0 +1,939 @@ +// 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<typeof enhancedDocumentSchema> + +// 상태 옵션 정의 +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<typeof Sheet> { + 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<File[]>([]) + 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<EnhancedDocumentSchema>({ + 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 ( + <Sheet {...props}> + <SheetContent className="flex flex-col gap-6 sm:max-w-2xl w-full"> + <SheetHeader className="text-left"> + <SheetTitle className="flex items-center gap-2"> + {mode === "upload" && <Upload className="w-5 h-5" />} + {mode === "approve" && <CheckCircle className="w-5 h-5" />} + {mode === "schedule" && <Calendar className="w-5 h-5" />} + {mode === "edit" && <FileText className="w-5 h-5" />} + {getSheetTitle()} + </SheetTitle> + <SheetDescription> + {getSheetDescription()} + </SheetDescription> + + {/* 프로젝트 타입 및 권한 표시 */} + <div className="flex items-center gap-2 pt-2"> + <Badge variant={projectType === "ship" ? "default" : "secondary"}> + {projectType === "ship" ? "조선 프로젝트" : "플랜트 프로젝트"} + </Badge> + {document?.isOverdue && ( + <Badge variant="destructive" className="flex items-center gap-1"> + <AlertTriangle className="w-3 h-3" /> + 지연 + </Badge> + )} + {document?.currentStagePriority === "HIGH" && ( + <Badge variant="destructive">높은 우선순위</Badge> + )} + </div> + </SheetHeader> + + <Form {...form}> + <form onSubmit={form.handleSubmit(onSubmit)} className="flex-1 flex flex-col"> + <Tabs value={activeTab} onValueChange={setActiveTab} className="flex-1 flex flex-col"> + <TabsList className="grid w-full grid-cols-4"> + <TabsTrigger value="info">기본정보</TabsTrigger> + <TabsTrigger value="schedule" disabled={!permissions.canSchedule}> + 일정관리 + </TabsTrigger> + <TabsTrigger value="upload" disabled={!permissions.canUpload}> + 리비전업로드 + </TabsTrigger> + <TabsTrigger value="approve" disabled={!permissions.canApprove}> + 승인처리 + </TabsTrigger> + </TabsList> + + {/* 기본 정보 탭 */} + <TabsContent value="info" className="flex-1 space-y-4"> + <ScrollArea className="h-full pr-4"> + <div className="space-y-4"> + <FormField + control={form.control} + name="docNumber" + render={({ field }) => ( + <FormItem> + <FormLabel>문서번호</FormLabel> + <FormControl> + <Input {...field} disabled={!permissions.canEdit} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="title" + render={({ field }) => ( + <FormItem> + <FormLabel>제목</FormLabel> + <FormControl> + <Input {...field} disabled={!permissions.canEdit} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + <div className="grid grid-cols-2 gap-4"> + <FormField + control={form.control} + name="pic" + render={({ field }) => ( + <FormItem> + <FormLabel>담당자 (PIC)</FormLabel> + <FormControl> + <Input {...field} disabled={!permissions.canEdit} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="status" + render={({ field }) => ( + <FormItem> + <FormLabel>상태</FormLabel> + <Select + onValueChange={field.onChange} + value={field.value} + disabled={!permissions.canEdit} + > + <FormControl> + <SelectTrigger> + <SelectValue /> + </SelectTrigger> + </FormControl> + <SelectContent> + {statusOptions.map((option) => ( + <SelectItem key={option.value} value={option.value}> + {option.label} + </SelectItem> + ))} + </SelectContent> + </Select> + <FormMessage /> + </FormItem> + )} + /> + </div> + + <FormField + control={form.control} + name="issuedDate" + render={({ field }) => ( + <FormItem> + <FormLabel>발행일</FormLabel> + <Popover> + <PopoverTrigger asChild> + <FormControl> + <Button + variant="outline" + className={cn( + "w-full pl-3 text-left font-normal", + !field.value && "text-muted-foreground" + )} + disabled={!permissions.canEdit} + > + {field.value ? ( + format(field.value, "yyyy년 MM월 dd일", { locale: ko }) + ) : ( + <span>날짜를 선택하세요</span> + )} + <Calendar className="ml-auto h-4 w-4 opacity-50" /> + </Button> + </FormControl> + </PopoverTrigger> + <PopoverContent className="w-auto p-0" align="start"> + <CalendarComponent + mode="single" + selected={field.value} + onSelect={field.onChange} + disabled={(date) => date > new Date()} + initialFocus + /> + </PopoverContent> + </Popover> + <FormMessage /> + </FormItem> + )} + /> + + {/* 현재 상태 정보 표시 */} + {document && ( + <div className="space-y-3 p-4 bg-gray-50 rounded-lg"> + <h4 className="font-medium flex items-center gap-2"> + <Clock className="w-4 h-4" /> + 현재 진행 상황 + </h4> + <div className="grid grid-cols-2 gap-4 text-sm"> + <div> + <span className="text-gray-500">현재 스테이지:</span> + <p className="font-medium">{document.currentStageName || "-"}</p> + </div> + <div> + <span className="text-gray-500">진행률:</span> + <p className="font-medium">{document.progressPercentage || 0}%</p> + </div> + <div> + <span className="text-gray-500">최신 리비전:</span> + <p className="font-medium">{document.latestRevision || "-"}</p> + </div> + <div> + <span className="text-gray-500">담당자:</span> + <p className="font-medium">{document.currentStageAssigneeName || "-"}</p> + </div> + </div> + </div> + )} + </div> + </ScrollArea> + </TabsContent> + + {/* 일정 관리 탭 */} + <TabsContent value="schedule" className="flex-1 space-y-4"> + <ScrollArea className="h-full pr-4"> + <div className="space-y-4"> + <div className="flex items-center justify-between"> + <h4 className="font-medium">스테이지 일정 관리</h4> + {projectType === "plant" && ( + <Button + type="button" + variant="outline" + size="sm" + onClick={addStage} + className="flex items-center gap-1" + > + <Plus className="w-4 h-4" /> + 스테이지 추가 + </Button> + )} + </div> + + {form.watch("stages")?.map((stage, index) => ( + <div key={index} className="p-4 border rounded-lg space-y-3"> + <div className="flex items-center justify-between"> + <h5 className="font-medium">스테이지 {index + 1}</h5> + {projectType === "plant" && ( + <Button + type="button" + variant="ghost" + size="sm" + onClick={() => removeStage(index)} + > + <X className="w-4 h-4" /> + </Button> + )} + </div> + + <div className="grid grid-cols-2 gap-3"> + <FormField + control={form.control} + name={`stages.${index}.stageName`} + render={({ field }) => ( + <FormItem> + <FormLabel>스테이지명</FormLabel> + <FormControl> + <Input {...field} disabled={projectType === "ship"} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name={`stages.${index}.priority`} + render={({ field }) => ( + <FormItem> + <FormLabel>우선순위</FormLabel> + <Select onValueChange={field.onChange} value={field.value}> + <FormControl> + <SelectTrigger> + <SelectValue /> + </SelectTrigger> + </FormControl> + <SelectContent> + {priorityOptions.map((option) => ( + <SelectItem key={option.value} value={option.value}> + {option.label} + </SelectItem> + ))} + </SelectContent> + </Select> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name={`stages.${index}.planDate`} + render={({ field }) => ( + <FormItem> + <FormLabel>계획일</FormLabel> + <Popover> + <PopoverTrigger asChild> + <FormControl> + <Button + variant="outline" + className={cn( + "w-full text-left font-normal", + !field.value && "text-muted-foreground" + )} + > + {field.value ? ( + format(field.value, "MM/dd", { locale: ko }) + ) : ( + <span>날짜 선택</span> + )} + <Calendar className="ml-auto h-4 w-4 opacity-50" /> + </Button> + </FormControl> + </PopoverTrigger> + <PopoverContent className="w-auto p-0" align="start"> + <CalendarComponent + mode="single" + selected={field.value} + onSelect={field.onChange} + initialFocus + /> + </PopoverContent> + </Popover> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name={`stages.${index}.assigneeName`} + render={({ field }) => ( + <FormItem> + <FormLabel>담당자</FormLabel> + <FormControl> + <Input {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + </div> + </div> + ))} + </div> + </ScrollArea> + </TabsContent> + + {/* 리비전 업로드 탭 */} + <TabsContent value="upload" className="flex-1 space-y-4"> + <ScrollArea className="h-full pr-4"> + <div className="space-y-4"> + <div className="grid grid-cols-2 gap-4"> + <FormField + control={form.control} + name="newRevision.stage" + render={({ field }) => ( + <FormItem> + <FormLabel>스테이지</FormLabel> + <FormControl> + <Input {...field} placeholder="예: Issued for Review" /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="newRevision.revision" + render={({ field }) => ( + <FormItem> + <FormLabel>리비전</FormLabel> + <FormControl> + <Input {...field} placeholder="예: A, B, 1, 2..." /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + </div> + + <FormField + control={form.control} + name="newRevision.uploaderName" + render={({ field }) => ( + <FormItem> + <FormLabel>업로더명 (선택)</FormLabel> + <FormControl> + <Input {...field} placeholder="업로더 이름을 입력하세요" /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="newRevision.comment" + render={({ field }) => ( + <FormItem> + <FormLabel>코멘트 (선택)</FormLabel> + <FormControl> + <Textarea {...field} placeholder="코멘트를 입력하세요" rows={3} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + {/* 파일 업로드 드롭존 */} + <FormField + control={form.control} + name="newRevision.attachments" + render={() => ( + <FormItem> + <FormLabel>파일 첨부</FormLabel> + <Dropzone + maxSize={3e9} // 3GB + multiple={true} + onDropAccepted={handleDropAccepted} + disabled={isUpdatePending} + > + <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> + <FileList className="max-h-[200px]"> + {selectedFiles.map((file, index) => ( + <FileListItem key={index} className="p-3"> + <FileListHeader> + <FileListIcon /> + <FileListInfo> + <FileListName>{file.name}</FileListName> + <FileListDescription> + {prettyBytes(file.size)} + </FileListDescription> + </FileListInfo> + <FileListAction + onClick={() => removeFile(index)} + disabled={isUpdatePending} + > + <X className="h-4 w-4" /> + </FileListAction> + </FileListHeader> + </FileListItem> + ))} + </FileList> + </div> + )} + + {/* 업로드 진행 상태 */} + {isUpdatePending && uploadProgress > 0 && ( + <div className="space-y-2"> + <div className="flex items-center gap-2"> + <Loader 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> + )} + </div> + </ScrollArea> + </TabsContent> + + {/* 승인 처리 탭 */} + <TabsContent value="approve" className="flex-1 space-y-4"> + <ScrollArea className="h-full pr-4"> + <div className="space-y-4"> + <div className="p-4 bg-blue-50 rounded-lg"> + <h4 className="font-medium mb-2 flex items-center gap-2"> + <CheckCircle className="w-4 h-4 text-blue-600" /> + 승인 대상 문서 + </h4> + <div className="text-sm space-y-1"> + <p><span className="font-medium">문서:</span> {document?.docNumber} - {document?.title}</p> + <p><span className="font-medium">현재 스테이지:</span> {document?.currentStageName}</p> + <p><span className="font-medium">최신 리비전:</span> {document?.latestRevision}</p> + <p><span className="font-medium">업로더:</span> {document?.latestRevisionUploaderName}</p> + </div> + </div> + + <div className="space-y-3"> + <div className="flex gap-3"> + <Button + type="button" + className="flex-1 bg-green-600 hover:bg-green-700" + onClick={() => { + // 승인 처리 로직 + console.log("승인 처리") + }} + > + <CheckCircle className="w-4 h-4 mr-2" /> + 승인 + </Button> + <Button + type="button" + variant="destructive" + className="flex-1" + onClick={() => { + // 반려 처리 로직 + console.log("반려 처리") + }} + > + <X className="w-4 h-4 mr-2" /> + 반려 + </Button> + </div> + + <FormField + control={form.control} + name="newRevision.comment" + render={({ field }) => ( + <FormItem> + <FormLabel>검토 의견</FormLabel> + <FormControl> + <Textarea + {...field} + placeholder="승인/반려 사유를 입력하세요" + rows={4} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + </div> + </div> + </ScrollArea> + </TabsContent> + </Tabs> + + <Separator /> + + <SheetFooter className="gap-2 pt-4"> + <SheetClose asChild> + <Button type="button" variant="outline"> + 취소 + </Button> + </SheetClose> + <Button + type="submit" + disabled={isUpdatePending} + className={mode === "approve" ? "bg-green-600 hover:bg-green-700" : ""} + > + {isUpdatePending && <Loader className="mr-2 size-4 animate-spin" />} + {mode === "upload" && <Upload className="mr-2 size-4" />} + {mode === "approve" && <CheckCircle className="mr-2 size-4" />} + {mode === "schedule" && <Calendar className="mr-2 size-4" />} + {mode === "edit" && <Save className="mr-2 size-4" />} + + {mode === "upload" ? "업로드" : + mode === "approve" ? "승인 처리" : + mode === "schedule" ? "일정 저장" : "저장"} + </Button> + </SheetFooter> + </form> + </Form> + </SheetContent> + </Sheet> + ) +}
\ No newline at end of file |
