diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-18 00:23:40 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-18 00:23:40 +0000 |
| commit | cf8dac0c6490469dab88a560004b0c07dbd48612 (patch) | |
| tree | b9e76061e80d868331e6b4277deecb9086f845f3 /lib/itb/table/create-purchase-request-dialog.tsx | |
| parent | e5745fc0268bbb5770bc14a55fd58a0ec30b466e (diff) | |
(대표님) rfq, 계약, 서명 등
Diffstat (limited to 'lib/itb/table/create-purchase-request-dialog.tsx')
| -rw-r--r-- | lib/itb/table/create-purchase-request-dialog.tsx | 995 |
1 files changed, 995 insertions, 0 deletions
diff --git a/lib/itb/table/create-purchase-request-dialog.tsx b/lib/itb/table/create-purchase-request-dialog.tsx new file mode 100644 index 00000000..27b8a342 --- /dev/null +++ b/lib/itb/table/create-purchase-request-dialog.tsx @@ -0,0 +1,995 @@ +// components/purchase-requests/create-purchase-request-dialog.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 { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +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 { + 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"; +import { CalendarIcon, X, Plus, FileText, Package, Trash2, Upload, FileIcon, AlertCircle, Paperclip } from "lucide-react"; +import { format } from "date-fns"; +import { cn } from "@/lib/utils"; +import { createPurchaseRequest } from "../service"; +import { toast } from "sonner"; +import { nanoid } from "nanoid"; + +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([]), + attachments: z.array(attachmentSchema).min(1, "최소 1개 이상의 파일을 첨부해주세요"), +}); + +type FormData = z.infer<typeof formSchema>; + +interface CreatePurchaseRequestDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onSuccess?: () => void; +} + +export function CreatePurchaseRequestDialog({ + open, + onOpenChange, + onSuccess, +}: CreatePurchaseRequestDialogProps) { + 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 [uploadedFiles, setUploadedFiles] = React.useState<File[]>([]); + const [isUploading, setIsUploading] = React.useState(false); + const [uploadProgress, setUploadProgress] = React.useState(0); + + const form = useForm<FormData>({ + resolver: zodResolver(formSchema), + mode: "onChange", + criteriaMode: "all", + defaultValues: { + requestTitle: "", + requestDescription: "", + projectId: undefined, + projectCode: "", + projectName: "", + projectCompany: "", + projectSite: "", + classNo: "", + packageNo: "", + packageName: "", + majorItemMaterialCategory: "", + majorItemMaterialDescription: "", + smCode: "", + estimatedBudget: "", + requestedDeliveryDate: undefined, + items: [], + attachments: [], + }, + }); + + const { fields, append, remove } = useFieldArray({ + control: form.control, + name: "items", + }); + + // 파일 업로드 핸들러 + const handleFileUpload = async (files: File[]) => { + if (files.length === 0) return; + + setIsUploading(true); + setUploadProgress(0); + + try { + const formData = new FormData(); + files.forEach(file => { + formData.append("files", file); + }); + + // 프로그레스 시뮬레이션 (실제로는 XMLHttpRequest 또는 fetch with progress를 사용) + const progressInterval = setInterval(() => { + setUploadProgress(prev => Math.min(prev + 10, 90)); + }, 200); + + const response = await fetch("/api/upload/purchase-request", { + method: "POST", + body: formData, + }); + + clearInterval(progressInterval); + setUploadProgress(100); + + if (!response.ok) { + throw new Error("파일 업로드 실패"); + } + + const data = await response.json(); + + // 업로드된 파일 정보를 상태에 추가 + const newFiles = [...uploadedFiles, ...data.files]; + setUploadedFiles(newFiles); + + // form의 attachments 필드 업데이트 + form.setValue("attachments", newFiles); + + toast.success(`${files.length}개 파일이 업로드되었습니다`); + } catch (error) { + console.error("Upload error:", error); + toast.error("파일 업로드 중 오류가 발생했습니다"); + } finally { + setIsUploading(false); + setUploadProgress(0); + } + }; + + // 파일 삭제 핸들러 + const handleFileRemove = (fileName: string) => { + const updatedFiles = uploadedFiles.filter(f => f.fileName !== fileName); + setUploadedFiles(updatedFiles); + form.setValue("attachments", updatedFiles); + }; + + // 프로젝트 선택 처리 + 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 onSubmit = async (values: FormData) => { + try { + setIsLoading(true); + + const result = await createPurchaseRequest({ + ...values, + items: values.items, + attachments: uploadedFiles, // 첨부파일 포함 + }); + + if (result.error) { + toast.error(result.error); + return; + } + + toast.success("구매 요청이 등록되었습니다"); + handleClose(); + onSuccess?.(); + } catch (error) { + toast.error("구매 요청 등록에 실패했습니다"); + console.error(error); + } finally { + setIsLoading(false); + } + }; + + const handleClose = () => { + form.reset({ + requestTitle: "", + requestDescription: "", + projectId: undefined, + projectCode: "", + projectName: "", + projectCompany: "", + projectSite: "", + classNo: "", + packageNo: "", + packageName: "", + majorItemMaterialCategory: "", + majorItemMaterialDescription: "", + smCode: "", + estimatedBudget: "", + requestedDeliveryDate: undefined, + items: [], + attachments: [], + }); + setSelectedPackage(null); + setSelectedMaterials([]); + setUploadedFiles([]); + setActiveTab("basic"); + setResetKey((k) => k + 1); + onOpenChange(false); + }; + + return ( + <Dialog open={open} onOpenChange={handleClose}> + <DialogContent key={resetKey} className="max-w-5xl h-[85vh] overflow-hidden flex flex-col min-h-0"> + <DialogHeader> + <DialogTitle>새 구매 요청</DialogTitle> + <DialogDescription> + 구매가 필요한 품목과 관련 정보를 입력해주세요 + </DialogDescription> + </DialogHeader> + + <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" /> + 첨부파일 + {uploadedFiles.length > 0 && ( + <span className="ml-2 rounded-full bg-primary px-2 py-0.5 text-xs text-primary-foreground"> + {uploadedFiles.length} + </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">첨부파일 업로드 <span className="text-red-500">*</span></CardTitle> + <CardDescription> + 설계 도면, 사양서, 견적서 등 구매 요청 관련 문서를 업로드하세요 (필수) + </CardDescription> + </CardHeader> + <CardContent className="space-y-4"> + {/* 필수 알림 */} + <FormField + control={form.control} + name="attachments" + render={({ field }) => ( + <FormItem> + <FormMessage /> + </FormItem> + )} + /> + + {/* 업로드 진행 상태 */} + {isUploading && ( + <Alert> + <AlertCircle className="h-4 w-4" /> + <AlertDescription className="space-y-2"> + <p>파일을 업로드 중입니다...</p> + <Progress value={uploadProgress} className="w-full" /> + </AlertDescription> + </Alert> + )} + + {/* Dropzone */} + <Dropzone + onDrop={handleFileUpload} + 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={isUploading} + > + <DropzoneInput /> + <DropzoneZone> + <DropzoneUploadIcon className="h-12 w-12 text-muted-foreground mb-4" /> + <DropzoneTitle> + 파일을 드래그하거나 클릭하여 선택하세요 + </DropzoneTitle> + <DropzoneDescription> + PDF, Excel, Word, 이미지 파일 등을 업로드할 수 있습니다 (최대 50MB) + </DropzoneDescription> + </DropzoneZone> + </Dropzone> + + {/* 업로드된 파일 목록 */} + {uploadedFiles.length > 0 && ( + <div className="space-y-2"> + <div className="flex items-center justify-between"> + <h4 className="text-sm font-medium">업로드된 파일</h4> + <span className="text-sm text-muted-foreground"> + 총 {uploadedFiles.length}개 파일 + </span> + </div> + + <FileList> + {uploadedFiles.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={() => handleFileRemove(file.fileName)} + disabled={isLoading || isUploading} + > + <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 className="font-medium">최소 1개 이상의 파일 첨부가 필수입니다</li> + <li>최대 파일 크기: 50MB</li> + <li>지원 형식: PDF, Excel, Word, 이미지 파일</li> + <li>여러 파일을 한 번에 선택하여 업로드할 수 있습니다</li> + </ul> + </AlertDescription> + </Alert> + </CardContent> + </Card> + </TabsContent> + </div> + </Tabs> + + <DialogFooter className="mt-6"> + <Button + type="button" + variant="outline" + onClick={handleClose} + disabled={isLoading || isUploading} + > + 취소 + </Button> + <Button + type="submit" + disabled={isLoading || isUploading || !form.formState.isValid} + > + {isLoading ? "등록 중..." : "구매요청 등록"} + </Button> + </DialogFooter> + </form> + </Form> + </DialogContent> + </Dialog> + ); +}
\ No newline at end of file |
