diff options
Diffstat (limited to 'lib/itb/table/edit-purchase-request-sheet.tsx')
| -rw-r--r-- | lib/itb/table/edit-purchase-request-sheet.tsx | 1081 |
1 files changed, 1081 insertions, 0 deletions
diff --git a/lib/itb/table/edit-purchase-request-sheet.tsx b/lib/itb/table/edit-purchase-request-sheet.tsx new file mode 100644 index 00000000..8a818ca5 --- /dev/null +++ b/lib/itb/table/edit-purchase-request-sheet.tsx @@ -0,0 +1,1081 @@ +// components/purchase-requests/edit-purchase-request-sheet.tsx +"use client"; + +import * as React from "react"; +import { useForm, useFieldArray } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import * as z from "zod"; +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, + SheetFooter, +} from "@/components/ui/sheet"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { Button } from "@/components/ui/button"; +import { Calendar } from "@/components/ui/calendar"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@/components/ui/tabs"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { ProjectSelector } from "@/components/ProjectSelector"; +import { PackageSelector } from "@/components/PackageSelector"; +import type { PackageItem } from "@/lib/items/service"; +import { MaterialGroupSelector } from "@/components/common/material/material-group-selector"; +import { CalendarIcon, X, Plus, FileText, Package, Trash2, Upload, FileIcon, AlertCircle, Paperclip, Save } from "lucide-react"; +import { format } from "date-fns"; +import { cn } from "@/lib/utils"; +import { updatePurchaseRequest } from "../service"; +import { toast } from "sonner"; +import { nanoid } from "nanoid"; +import type { PurchaseRequestView } from "@/db/schema"; +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 { Progress } from "@/components/ui/progress"; +import { Alert, AlertDescription } from "@/components/ui/alert"; + +const attachmentSchema = z.object({ + fileName: z.string(), + originalFileName: z.string(), + filePath: z.string(), + fileSize: z.number(), + fileType: z.string(), +}); + +const itemSchema = z.object({ + id: z.string(), + itemCode: z.string(), + itemName: z.string().min(1, "아이템명을 입력해주세요"), + specification: z.string().default(""), + quantity: z.coerce.number().min(1, "수량은 1 이상이어야 합니다"), + unit: z.string().min(1, "단위를 입력해주세요"), + estimatedUnitPrice: z.coerce.number().optional(), + remarks: z.string().optional(), +}); + +const formSchema = z.object({ + requestTitle: z.string().min(1, "요청 제목을 입력해주세요"), + requestDescription: z.string().optional(), + projectId: z.number({ + required_error: "프로젝트를 선택해주세요", + }), + projectCode: z.string().min(1), + projectName: z.string().min(1), + projectCompany: z.string().optional(), + projectSite: z.string().optional(), + classNo: z.string().optional(), + packageNo: z.string({ + required_error: "패키지를 선택해주세요", + }).min(1, "패키지를 선택해주세요"), + packageName: z.string().min(1), + majorItemMaterialCategory: z.string({ + required_error: "자재그룹을 선택해주세요", + }).min(1, "자재그룹을 선택해주세요"), + majorItemMaterialDescription: z.string().min(1), + smCode: z.string({ + required_error: "SM 코드가 필요합니다", + }).min(1, "SM 코드가 필요합니다"), + estimatedBudget: z.string().optional(), + requestedDeliveryDate: z.date().optional(), + items: z.array(itemSchema).optional().default([]), +}); + +type FormData = z.infer<typeof formSchema>; + +interface EditPurchaseRequestSheetProps { + request: PurchaseRequestView; + open: boolean; + onOpenChange: (open: boolean) => void; + onSuccess?: () => void; +} + +export function EditPurchaseRequestSheet({ + request, + open, + onOpenChange, + onSuccess, +}: EditPurchaseRequestSheetProps) { + const [isLoading, setIsLoading] = React.useState(false); + const [activeTab, setActiveTab] = React.useState("basic"); + const [selectedPackage, setSelectedPackage] = React.useState<PackageItem | null>(null); + const [selectedMaterials, setSelectedMaterials] = React.useState<any[]>([]); + const [resetKey, setResetKey] = React.useState(0); + const [newFiles, setNewFiles] = React.useState<File[]>([]); // 새로 추가할 파일 + const [existingFiles, setExistingFiles] = React.useState<z.infer<typeof attachmentSchema>[]>([]); // 기존 파일 + const [isUploading, setIsUploading] = React.useState(false); + const [uploadProgress, setUploadProgress] = React.useState(0); + + // 기존 아이템 처리 + const existingItems = React.useMemo(() => { + if (!request.items || !Array.isArray(request.items) || request.items.length === 0) { + return []; + } + return request.items.map(item => ({ + ...item, + id: item.id || nanoid(), + })); + }, [request.items]); + + // 기존 첨부파일 로드 (실제로는 API에서 가져와야 함) + React.useEffect(() => { + // TODO: 기존 첨부파일 로드 API 호출 + // setExistingFiles(request.attachments || []); + }, [request.id]); + + // 기존 자재그룹 데이터 설정 + React.useEffect(() => { + if (request.majorItemMaterialCategory && request.majorItemMaterialDescription) { + setSelectedMaterials([{ + materialGroupCode: request.majorItemMaterialCategory, + materialGroupDescription: request.majorItemMaterialDescription, + displayText:`${request.majorItemMaterialCategory} - ${request.majorItemMaterialDescription}` + }]); + } + }, [request.majorItemMaterialCategory, request.majorItemMaterialDescription]); + + // 기존 패키지 데이터 설정 + React.useEffect(() => { + if (request.packageNo && request.packageName) { + setSelectedPackage({ + packageCode: request.packageNo, + description: request.packageName, + smCode: request.smCode || "", + } as PackageItem); + } + }, [request.packageNo, request.packageName, request.smCode]); + + const form = useForm<FormData>({ + resolver: zodResolver(formSchema), + mode: "onChange", + criteriaMode: "all", + defaultValues: { + requestTitle: request.requestTitle || "", + requestDescription: request.requestDescription || "", + projectId: request.projectId || undefined, + projectCode: request.projectCode || "", + projectName: request.projectName || "", + projectCompany: request.projectCompany || "", + projectSite: request.projectSite || "", + classNo: request.classNo || "", + packageNo: request.packageNo || "", + packageName: request.packageName || "", + majorItemMaterialCategory: request.majorItemMaterialCategory || "", + majorItemMaterialDescription: request.majorItemMaterialDescription || "", + smCode: request.smCode || "", + estimatedBudget: request.estimatedBudget || "", + requestedDeliveryDate: request.requestedDeliveryDate + ? new Date(request.requestedDeliveryDate) + : undefined, + items: existingItems, + }, + }); + + const { fields, append, remove } = useFieldArray({ + control: form.control, + name: "items", + }); + + // 프로젝트 선택 처리 + const handleProjectSelect = (project: any) => { + form.setValue("projectId", project.id); + form.setValue("projectCode", project.projectCode); + form.setValue("projectName", project.projectName); + form.setValue("projectCompany", project.projectCompany); + form.setValue("projectSite", project.projectSite); + + // 프로젝트 변경시 패키지 초기화 + setSelectedPackage(null); + form.setValue("packageNo", ""); + form.setValue("packageName", ""); + form.setValue("smCode", ""); + + // requestTitle 업데이트 + updateRequestTitle(project.projectName, "", ""); + }; + + // 패키지 선택 처리 + const handlePackageSelect = (packageItem: PackageItem) => { + setSelectedPackage(packageItem); + form.setValue("packageNo", packageItem.packageCode); + form.setValue("packageName", packageItem.description); + + // SM 코드가 패키지에 있으면 자동 설정 + if (packageItem.smCode) { + form.setValue("smCode", packageItem.smCode); + } + + // requestTitle 업데이트 + const projectName = form.getValues("projectName"); + const materialDesc = form.getValues("majorItemMaterialDescription"); + updateRequestTitle(projectName, packageItem.description, materialDesc); + }; + + // 자재그룹 선택 처리 + const handleMaterialsChange = (materials: any[]) => { + setSelectedMaterials(materials); + if (materials.length > 0) { + const material = materials[0]; // single select이므로 첫번째 항목만 + form.setValue("majorItemMaterialCategory", material.materialGroupCode); + form.setValue("majorItemMaterialDescription", material.materialGroupDescription); + + // requestTitle 업데이트 + const projectName = form.getValues("projectName"); + const packageName = form.getValues("packageName"); + updateRequestTitle(projectName, packageName, material.materialGroupDescription); + } else { + form.setValue("majorItemMaterialCategory", ""); + form.setValue("majorItemMaterialDescription", ""); + } + }; + + // requestTitle 자동 생성 + const updateRequestTitle = (projectName: string, packageName: string, materialDesc: string) => { + const parts = []; + if (projectName) parts.push(projectName); + if (packageName) parts.push(packageName); + if (materialDesc) parts.push(materialDesc); + + if (parts.length > 0) { + const title = `${parts.join(" - ")} 구매 요청`; + form.setValue("requestTitle", title); + } + }; + + const addItem = () => { + append({ + id: nanoid(), + itemCode: "", + itemName: "", + specification: "", + quantity: 1, + unit: "개", + estimatedUnitPrice: undefined, + remarks: "", + }); + }; + + const calculateTotal = () => { + const items = form.watch("items"); + return items?.reduce((sum, item) => { + const subtotal = (item.quantity || 0) * (item.estimatedUnitPrice || 0); + return sum + subtotal; + }, 0) || 0; + }; + + // 파일 추가 핸들러 (로컬 상태로만 저장) + const handleFileAdd = (files: File[]) => { + if (files.length === 0) return; + + // 중복 파일 체크 + const duplicates = files.filter(file => + newFiles.some(existing => existing.name === file.name) + ); + + if (duplicates.length > 0) { + toast.warning(`${duplicates.length}개 파일이 이미 추가되어 있습니다`); + } + + const uniqueFiles = files.filter(file => + !newFiles.some(existing => existing.name === file.name) + ); + + if (uniqueFiles.length > 0) { + setNewFiles(prev => [...prev, ...uniqueFiles]); + toast.success(`${uniqueFiles.length}개 파일이 추가되었습니다`); + } + }; + + // 새 파일 삭제 핸들러 + const handleNewFileRemove = (fileName: string) => { + setNewFiles(prev => prev.filter(f => f.name !== fileName)); + }; + + // 기존 파일 삭제 핸들러 (실제로는 서버에 삭제 요청) + const handleExistingFileRemove = (fileName: string) => { + // TODO: 서버에 삭제 표시 + setExistingFiles(prev => prev.filter(f => f.fileName !== fileName)); + }; + + const onSubmit = async (values: FormData) => { + try { + setIsLoading(true); + + // 새 파일이 있으면 먼저 업로드 + let uploadedAttachments: z.infer<typeof attachmentSchema>[] = []; + + if (newFiles.length > 0) { + setIsUploading(true); + setUploadProgress(10); + + const formData = new FormData(); + newFiles.forEach(file => { + formData.append("files", file); + }); + + setUploadProgress(30); + + try { + const response = await fetch("/api/upload/purchase-request", { + method: "POST", + body: formData, + }); + + setUploadProgress(60); + + if (!response.ok) { + throw new Error("파일 업로드 실패"); + } + + const data = await response.json(); + uploadedAttachments = data.files; + setUploadProgress(80); + } catch (error) { + toast.error("파일 업로드 중 오류가 발생했습니다"); + setIsUploading(false); + setIsLoading(false); + setUploadProgress(0); + return; + } + } + + setUploadProgress(90); + + // 구매 요청 업데이트 (기존 파일 + 새로 업로드된 파일) + const result = await updatePurchaseRequest(request.id, { + ...values, + items: values.items, + attachments: [...existingFiles, ...uploadedAttachments], + }); + + setUploadProgress(100); + + if (result.error) { + toast.error(result.error); + return; + } + + toast.success("구매 요청이 수정되었습니다"); + handleClose(); + onSuccess?.(); + } catch (error) { + toast.error("구매 요청 수정에 실패했습니다"); + console.error(error); + } finally { + setIsLoading(false); + setIsUploading(false); + setUploadProgress(0); + } + }; + + const handleClose = () => { + form.reset(); + setSelectedPackage(null); + setSelectedMaterials([]); + setNewFiles([]); + setExistingFiles([]); + setActiveTab("basic"); + setResetKey((k) => k + 1); + onOpenChange(false); + }; + + const canEdit = request.status === "작성중"; + const totalFileCount = existingFiles.length + newFiles.length; + + return ( + <Sheet open={open} onOpenChange={handleClose}> + <SheetContent key={resetKey} className="w-[900px] max-w-[900px] overflow-hidden flex flex-col min-h-0" style={{width:900, maxWidth:900}}> + <SheetHeader> + <SheetTitle>구매 요청 수정</SheetTitle> + <SheetDescription> + 요청번호: {request.requestCode} | 상태: {request.status} + </SheetDescription> + </SheetHeader> + + {!canEdit ? ( + <div className="flex-1 flex items-center justify-center"> + <Card> + <CardContent className="pt-6"> + <p className="text-center text-muted-foreground"> + 작성중 상태의 요청만 수정할 수 있습니다. + </p> + </CardContent> + </Card> + </div> + ) : ( + <Form {...form}> + <form onSubmit={form.handleSubmit(onSubmit)} className="flex-1 overflow-hidden flex flex-col min-h-0"> + <Tabs value={activeTab} onValueChange={setActiveTab} className="flex-1 flex flex-col min-h-0"> + <TabsList className="grid w-full grid-cols-3 flex-shrink-0"> + <TabsTrigger value="basic"> + <FileText className="mr-2 h-4 w-4" /> + 기본 정보 + </TabsTrigger> + <TabsTrigger value="items"> + <Package className="mr-2 h-4 w-4" /> + 품목 정보 + {fields.length > 0 && ( + <span className="ml-2 rounded-full bg-primary px-2 py-0.5 text-xs text-primary-foreground"> + {fields.length} + </span> + )} + </TabsTrigger> + <TabsTrigger value="files"> + <Paperclip className="mr-2 h-4 w-4" /> + 첨부파일 + {totalFileCount > 0 && ( + <span className="ml-2 rounded-full bg-primary px-2 py-0.5 text-xs text-primary-foreground"> + {totalFileCount} + </span> + )} + </TabsTrigger> + </TabsList> + + <div className="flex-1 overflow-y-auto px-1 min-h-0"> + <TabsContent value="basic" className="space-y-6 mt-6"> + {/* 프로젝트 정보 */} + <Card> + <CardHeader> + <CardTitle className="text-base">프로젝트 정보 <span className="text-red-500">*</span></CardTitle> + <CardDescription>프로젝트, 패키지, 자재그룹을 순서대로 선택하세요</CardDescription> + </CardHeader> + <CardContent className="space-y-4"> + {/* 프로젝트 선택 */} + <FormField + control={form.control} + name="projectId" + render={({ field }) => ( + <FormItem> + <FormLabel>프로젝트 <span className="text-red-500">*</span></FormLabel> + <ProjectSelector + key={`project-${resetKey}`} + selectedProjectId={field.value} + onProjectSelect={handleProjectSelect} + placeholder="프로젝트를 검색하여 선택..." + /> + <FormMessage /> + </FormItem> + )} + /> + + {/* 패키지 선택 */} + <FormField + control={form.control} + name="packageNo" + render={({ field }) => ( + <FormItem> + <FormLabel>패키지 <span className="text-red-500">*</span></FormLabel> + <PackageSelector + projectNo={form.watch("projectCode")} + selectedPackage={selectedPackage} + onPackageSelect={handlePackageSelect} + placeholder="패키지 선택..." + /> + <FormMessage /> + </FormItem> + )} + /> + + {/* 자재그룹 선택 */} + <FormField + control={form.control} + name="majorItemMaterialCategory" + render={({ field }) => ( + <FormItem> + <FormLabel>자재그룹 <span className="text-red-500">*</span></FormLabel> + <MaterialGroupSelector + selectedMaterials={selectedMaterials} + onMaterialsChange={handleMaterialsChange} + singleSelect={true} + placeholder="자재그룹을 검색하세요..." + noValuePlaceHolder="자재그룹을 선택해주세요" + /> + <FormMessage /> + </FormItem> + )} + /> + + {/* SM 코드 */} + <FormField + control={form.control} + name="smCode" + render={({ field }) => ( + <FormItem> + <FormLabel>SM 코드 <span className="text-red-500">*</span></FormLabel> + <FormControl> + <Input + placeholder="SM 코드 입력" + {...field} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + {/* 선택된 정보 표시 */} + {form.watch("projectId") && ( + <div className="grid grid-cols-2 gap-4 pt-4 border-t"> + <div> + <p className="text-sm text-muted-foreground">프로젝트 코드</p> + <p className="font-medium">{form.watch("projectCode") || "-"}</p> + </div> + <div> + <p className="text-sm text-muted-foreground">프로젝트명</p> + <p className="font-medium">{form.watch("projectName") || "-"}</p> + </div> + <div> + <p className="text-sm text-muted-foreground">발주처</p> + <p className="font-medium">{form.watch("projectCompany") || "-"}</p> + </div> + <div> + <p className="text-sm text-muted-foreground">현장</p> + <p className="font-medium">{form.watch("projectSite") || "-"}</p> + </div> + </div> + )} + </CardContent> + </Card> + + {/* 기본 정보 */} + <div className="space-y-4"> + <FormField + control={form.control} + name="requestTitle" + render={({ field }) => ( + <FormItem> + <FormLabel>요청 제목 <span className="text-red-500">*</span></FormLabel> + <FormControl> + <Input + placeholder="예: 프로젝트 A용 밸브 구매" + {...field} + /> + </FormControl> + <FormDescription> + 자동 생성된 제목을 수정할 수 있습니다 + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="requestDescription" + render={({ field }) => ( + <FormItem> + <FormLabel>요청 설명</FormLabel> + <FormControl> + <Textarea + placeholder="구매 요청에 대한 상세 설명을 입력하세요" + className="min-h-[100px]" + {...field} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + </div> + + {/* 예산 및 납기 */} + <div className="space-y-4"> + <h3 className="text-sm font-medium">예산 및 납기</h3> + <div className="grid grid-cols-2 gap-4"> + <FormField + control={form.control} + name="estimatedBudget" + render={({ field }) => ( + <FormItem> + <FormLabel>예상 예산</FormLabel> + <FormControl> + <Input placeholder="예: 10,000,000원" {...field} /> + </FormControl> + <FormDescription> + 전체 구매 예상 예산을 입력하세요 + </FormDescription> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="requestedDeliveryDate" + 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" + )} + > + {field.value ? ( + format(field.value, "yyyy-MM-dd") + ) : ( + <span>날짜 선택</span> + )} + <CalendarIcon className="ml-auto h-4 w-4 opacity-50" /> + </Button> + </FormControl> + </PopoverTrigger> + <PopoverContent className="w-auto p-0" align="start"> + <Calendar + mode="single" + selected={field.value} + onSelect={field.onChange} + disabled={(date) => date < new Date()} + /> + </PopoverContent> + </Popover> + <FormDescription> + 자재가 필요한 날짜를 선택하세요 + </FormDescription> + </FormItem> + )} + /> + </div> + </div> + </TabsContent> + + <TabsContent value="items" className="space-y-4 mt-6"> + {/* 품목 목록 */} + <div className="flex justify-between items-center"> + <h3 className="text-sm font-medium">품목 목록</h3> + <Button + type="button" + variant="outline" + size="sm" + onClick={addItem} + > + <Plus className="mr-2 h-4 w-4" /> + 품목 추가 + </Button> + </div> + + {fields.length === 0 ? ( + <Card> + <CardContent className="flex flex-col items-center justify-center py-8"> + <Package className="h-12 w-12 text-muted-foreground mb-4" /> + <p className="text-sm text-muted-foreground mb-4"> + 아직 추가된 품목이 없습니다 + </p> + <Button + type="button" + variant="outline" + size="sm" + onClick={addItem} + > + <Plus className="mr-2 h-4 w-4" /> + 첫 품목 추가 + </Button> + </CardContent> + </Card> + ) : ( + <div className="border rounded-lg"> + <Table> + <TableHeader> + <TableRow> + <TableHead className="w-[120px]">아이템 코드</TableHead> + <TableHead className="w-[150px]">아이템명 <span className="text-red-500">*</span></TableHead> + <TableHead>사양</TableHead> + <TableHead className="w-[80px]">수량 <span className="text-red-500">*</span></TableHead> + <TableHead className="w-[80px]">단위 <span className="text-red-500">*</span></TableHead> + <TableHead className="w-[120px]">예상 단가</TableHead> + <TableHead className="w-[120px]">예상 금액</TableHead> + <TableHead>비고</TableHead> + <TableHead className="w-[50px]"></TableHead> + </TableRow> + </TableHeader> + <TableBody> + {fields.map((field, index) => ( + <TableRow key={field.id}> + <TableCell> + <FormField + control={form.control} + name={`items.${index}.itemCode`} + render={({ field }) => ( + <FormItem> + <FormControl> + <Input + placeholder="코드" + {...field} + className="h-8" + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + </TableCell> + <TableCell> + <FormField + control={form.control} + name={`items.${index}.itemName`} + render={({ field }) => ( + <FormItem> + <FormControl> + <Input + placeholder="아이템명" + {...field} + className="h-8" + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + </TableCell> + <TableCell> + <FormField + control={form.control} + name={`items.${index}.specification`} + render={({ field }) => ( + <FormItem> + <FormControl> + <Input + placeholder="사양" + {...field} + className="h-8" + /> + </FormControl> + </FormItem> + )} + /> + </TableCell> + <TableCell> + <FormField + control={form.control} + name={`items.${index}.quantity`} + render={({ field }) => ( + <FormItem> + <FormControl> + <Input + type="number" + min="1" + {...field} + className="h-8" + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + </TableCell> + <TableCell> + <FormField + control={form.control} + name={`items.${index}.unit`} + render={({ field }) => ( + <FormItem> + <FormControl> + <Input + placeholder="개" + {...field} + className="h-8" + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + </TableCell> + <TableCell> + <FormField + control={form.control} + name={`items.${index}.estimatedUnitPrice`} + render={({ field }) => ( + <FormItem> + <FormControl> + <Input + type="number" + min="0" + placeholder="0" + {...field} + className="h-8" + /> + </FormControl> + </FormItem> + )} + /> + </TableCell> + <TableCell> + <div className="text-right font-medium"> + {new Intl.NumberFormat('ko-KR').format( + (form.watch(`items.${index}.quantity`) || 0) * + (form.watch(`items.${index}.estimatedUnitPrice`) || 0) + )} + </div> + </TableCell> + <TableCell> + <FormField + control={form.control} + name={`items.${index}.remarks`} + render={({ field }) => ( + <FormItem> + <FormControl> + <Input + placeholder="비고" + {...field} + className="h-8" + /> + </FormControl> + </FormItem> + )} + /> + </TableCell> + <TableCell> + <Button + type="button" + variant="ghost" + size="icon" + className="h-8 w-8" + onClick={() => remove(index)} + disabled={fields.length === 1} + > + <Trash2 className="h-4 w-4" /> + </Button> + </TableCell> + </TableRow> + ))} + </TableBody> + </Table> + + <div className="flex justify-between items-center p-4 border-t bg-muted/30"> + <div className="text-sm text-muted-foreground"> + 총 {fields.length}개 품목 + </div> + <div className="text-right"> + <p className="text-sm text-muted-foreground">예상 총액</p> + <p className="text-lg font-semibold"> + {new Intl.NumberFormat('ko-KR', { + style: 'currency', + currency: 'KRW' + }).format(calculateTotal())} + </p> + </div> + </div> + </div> + )} + </TabsContent> + + <TabsContent value="files" className="space-y-4 mt-6"> + {/* 파일 업로드 영역 */} + <Card> + <CardHeader> + <CardTitle className="text-base">첨부파일 관리</CardTitle> + <CardDescription> + 설계 도면, 사양서, 견적서 등 구매 요청 관련 문서를 관리하세요 + </CardDescription> + </CardHeader> + <CardContent className="space-y-4"> + {/* 업로드 진행 상태 */} + {isUploading && ( + <Alert> + <AlertCircle className="h-4 w-4" /> + <AlertDescription className="space-y-2"> + <p>파일을 업로드하고 데이터를 저장 중입니다...</p> + <Progress value={uploadProgress} className="w-full" /> + </AlertDescription> + </Alert> + )} + + {/* 기존 파일 목록 */} + {existingFiles.length > 0 && ( + <div className="space-y-2"> + <h4 className="text-sm font-medium">기존 첨부파일</h4> + <FileList> + {existingFiles.map((file) => ( + <FileListItem key={file.fileName}> + <FileListIcon> + <FileIcon className="h-4 w-4" /> + </FileListIcon> + <FileListInfo> + <FileListHeader> + <FileListName> + {file.originalFileName} + </FileListName> + <FileListSize> + {file.fileSize} + </FileListSize> + </FileListHeader> + + </FileListInfo> + <FileListAction> + <Button + variant="ghost" + size="icon" + className="h-8 w-8" + onClick={() => handleExistingFileRemove(file.fileName)} + disabled={isLoading} + > + <X className="h-4 w-4" /> + </Button> + </FileListAction> + </FileListItem> + ))} + </FileList> + </div> + )} + + {/* Dropzone */} + <Dropzone + onDrop={handleFileAdd} + accept={{ + 'application/pdf': ['.pdf'], + 'application/vnd.ms-excel': ['.xls'], + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx'], + 'application/msword': ['.doc'], + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'], + 'image/*': ['.png', '.jpg', '.jpeg', '.gif'], + 'application/x-zip-compressed': ['.zip'], + 'application/x-rar-compressed': ['.rar'], + }} + maxSize={50 * 1024 * 1024} // 50MB + disabled={isLoading} + > + <DropzoneInput /> + <DropzoneZone> + <DropzoneUploadIcon className="h-12 w-12 text-muted-foreground mb-4" /> + <DropzoneTitle> + 파일을 드래그하거나 클릭하여 선택하세요 + </DropzoneTitle> + <DropzoneDescription> + PDF, Excel, Word, 이미지 파일 등을 업로드할 수 있습니다 (최대 50MB) + </DropzoneDescription> + </DropzoneZone> + </Dropzone> + + {/* 새로 추가할 파일 목록 */} + {newFiles.length > 0 && ( + <div className="space-y-2"> + <h4 className="text-sm font-medium text-blue-600">새로 추가할 파일</h4> + <FileList> + {newFiles.map((file) => ( + <FileListItem key={file.name} className="bg-blue-50"> + <FileListIcon> + <FileIcon className="h-4 w-4 text-blue-600" /> + </FileListIcon> + <FileListInfo> + <FileListHeader> + <FileListName> + {file.name} + </FileListName> + <FileListSize> + {file.size} + </FileListSize> + </FileListHeader> + <FileListDescription> + {file.type || "application/octet-stream"} + </FileListDescription> + </FileListInfo> + <FileListAction> + <Button + variant="ghost" + size="icon" + className="h-8 w-8" + onClick={() => handleNewFileRemove(file.name)} + disabled={isLoading} + > + <X className="h-4 w-4" /> + </Button> + </FileListAction> + </FileListItem> + ))} + </FileList> + </div> + )} + + {/* 파일 업로드 안내 */} + <Alert> + <AlertCircle className="h-4 w-4" /> + <AlertDescription> + <ul className="list-disc list-inside space-y-1 text-sm"> + <li>새 파일은 수정 완료 시 서버로 업로드됩니다</li> + <li>최대 파일 크기: 50MB</li> + <li>지원 형식: PDF, Excel, Word, 이미지 파일</li> + <li>여러 파일을 한 번에 선택하여 추가할 수 있습니다</li> + </ul> + </AlertDescription> + </Alert> + </CardContent> + </Card> + </TabsContent> + </div> + </Tabs> + + <SheetFooter className="mt-6"> + <Button + type="button" + variant="outline" + onClick={handleClose} + disabled={isLoading || isUploading} + > + 취소 + </Button> + <Button type="submit" disabled={isLoading || isUploading || !form.formState.isValid}> + <Save className="mr-2 h-4 w-4" /> + {isLoading ? "수정 중..." : "수정 완료"} + </Button> + </SheetFooter> + </form> + </Form> + )} + </SheetContent> + </Sheet> + ); +}
\ No newline at end of file |
