diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-06-17 09:02:32 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-06-17 09:02:32 +0000 |
| commit | 7a1524ba54f43d0f2a19e4bca2c6a2e0b01c5ef1 (patch) | |
| tree | daa214d404c7fc78b32419a028724e5671a6c7a4 /lib/b-rfq/vendor-response/upload-response-dialog.tsx | |
| parent | fa6a6093014c5d60188edfc9c4552e81c4b97bd1 (diff) | |
(대표님) 20250617 18시 작업사항
Diffstat (limited to 'lib/b-rfq/vendor-response/upload-response-dialog.tsx')
| -rw-r--r-- | lib/b-rfq/vendor-response/upload-response-dialog.tsx | 325 |
1 files changed, 325 insertions, 0 deletions
diff --git a/lib/b-rfq/vendor-response/upload-response-dialog.tsx b/lib/b-rfq/vendor-response/upload-response-dialog.tsx new file mode 100644 index 00000000..b4b306d6 --- /dev/null +++ b/lib/b-rfq/vendor-response/upload-response-dialog.tsx @@ -0,0 +1,325 @@ +// components/rfq/upload-response-dialog.tsx +"use client"; + +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import * as z from "zod"; +import { Upload, FileText, X, Loader2 } from "lucide-react"; +import { useToast } from "@/hooks/use-toast" +import { useRouter } from "next/navigation"; + +const uploadFormSchema = z.object({ + files: z.array(z.instanceof(File)).min(1, "최소 1개의 파일을 선택해주세요"), + responseComment: z.string().optional(), + vendorComment: z.string().optional(), +}); + +type UploadFormData = z.infer<typeof uploadFormSchema>; + +interface UploadResponseDialogProps { + responseId: number; + attachmentType: string; + serialNo: string; + currentRevision: string; + trigger?: React.ReactNode; + onSuccess?: () => void; +} + +export function UploadResponseDialog({ + responseId, + attachmentType, + serialNo, + currentRevision, + trigger, + onSuccess, +}: UploadResponseDialogProps) { + const [open, setOpen] = useState(false); + const [isUploading, setIsUploading] = useState(false); + const { toast } = useToast(); + const router = useRouter(); + + const form = useForm<UploadFormData>({ + resolver: zodResolver(uploadFormSchema), + defaultValues: { + files: [], + responseComment: "", + vendorComment: "", + }, + }); + + const selectedFiles = form.watch("files"); + + const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => { + const files = Array.from(e.target.files || []); + if (files.length > 0) { + form.setValue("files", files); + } + }; + + const removeFile = (index: number) => { + const currentFiles = form.getValues("files"); + const newFiles = currentFiles.filter((_, i) => i !== index); + form.setValue("files", newFiles); + }; + + const formatFileSize = (bytes: number): string => { + 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]; + }; + + const handleOpenChange = (newOpen: boolean) => { + setOpen(newOpen); + // 다이얼로그가 닫힐 때 form 리셋 + if (!newOpen) { + form.reset(); + } + }; + + const handleCancel = () => { + form.reset(); + setOpen(false); + }; + + const onSubmit = async (data: UploadFormData) => { + setIsUploading(true); + + try { + // 1. 각 파일을 업로드 API로 전송 + const uploadedFiles = []; + + for (const file of data.files) { + const formData = new FormData(); + formData.append("file", file); + formData.append("responseId", responseId.toString()); + formData.append("description", ""); // 필요시 파일별 설명 추가 가능 + + const uploadResponse = await fetch("/api/vendor-responses/upload", { + method: "POST", + body: formData, + }); + + if (!uploadResponse.ok) { + const error = await uploadResponse.json(); + throw new Error(error.message || "파일 업로드 실패"); + } + + const uploadResult = await uploadResponse.json(); + uploadedFiles.push(uploadResult); + } + + // 2. vendor response 상태 업데이트 (서버에서 자동으로 리비전 증가) + const updateResponse = await fetch("/api/vendor-responses/update", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + responseId, + responseStatus: "RESPONDED", + // respondedRevision 제거 - 서버에서 자동 처리 + responseComment: data.responseComment, + vendorComment: data.vendorComment, + respondedAt: new Date().toISOString(), + }), + }); + + if (!updateResponse.ok) { + const error = await updateResponse.json(); + throw new Error(error.message || "응답 상태 업데이트 실패"); + } + + const updateResult = await updateResponse.json(); + + toast({ + title: "업로드 완료", + description: `${data.files.length}개 파일이 성공적으로 업로드되었습니다. (${updateResult.newRevision})`, + }); + + setOpen(false); + form.reset(); + + router.refresh(); + onSuccess?.(); + + } catch (error) { + console.error("Upload error:", error); + toast({ + title: "업로드 실패", + description: error instanceof Error ? error.message : "알 수 없는 오류가 발생했습니다.", + variant: "destructive", + }); + } finally { + setIsUploading(false); + } + }; + + return ( + <Dialog open={open} onOpenChange={handleOpenChange}> + <DialogTrigger asChild> + {trigger || ( + <Button size="sm"> + <Upload className="h-3 w-3 mr-1" /> + 업로드 + </Button> + )} + </DialogTrigger> + <DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto"> + <DialogHeader> + <DialogTitle className="flex items-center gap-2"> + <Upload className="h-5 w-5" /> + 응답 파일 업로드 + </DialogTitle> + <div className="flex items-center gap-2 text-sm text-muted-foreground"> + <Badge variant="outline">{serialNo}</Badge> + <span>{attachmentType}</span> + <Badge variant="secondary">{currentRevision}</Badge> + <span className="text-xs text-blue-600">→ 벤더 응답 리비전 자동 증가</span> + </div> + </DialogHeader> + + <Form {...form}> + <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6"> + {/* 파일 선택 */} + <FormField + control={form.control} + name="files" + render={({ field }) => ( + <FormItem> + <FormLabel>파일 선택</FormLabel> + <FormControl> + <div className="space-y-4"> + <Input + type="file" + multiple + onChange={handleFileSelect} + accept=".pdf,.doc,.docx,.xls,.xlsx,.png,.jpg,.jpeg,.zip,.rar" + className="cursor-pointer" + /> + <div className="text-xs text-muted-foreground"> + 지원 파일: PDF, DOC, DOCX, XLS, XLSX, PNG, JPG, ZIP, RAR (최대 10MB) + </div> + </div> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + {/* 선택된 파일 목록 */} + {selectedFiles.length > 0 && ( + <div className="space-y-2"> + <div className="text-sm font-medium">선택된 파일 ({selectedFiles.length}개)</div> + <div className="space-y-2 max-h-40 overflow-y-auto"> + {selectedFiles.map((file, index) => ( + <div + key={index} + className="flex items-center justify-between p-3 bg-muted/50 rounded-lg" + > + <div className="flex items-center gap-2 flex-1 min-w-0"> + <FileText className="h-4 w-4 text-muted-foreground flex-shrink-0" /> + <div className="min-w-0 flex-1"> + <div className="text-sm font-medium truncate">{file.name}</div> + <div className="text-xs text-muted-foreground"> + {formatFileSize(file.size)} + </div> + </div> + </div> + <Button + type="button" + variant="ghost" + size="sm" + onClick={() => removeFile(index)} + className="flex-shrink-0 ml-2" + > + <X className="h-4 w-4" /> + </Button> + </div> + ))} + </div> + </div> + )} + + {/* 응답 코멘트 */} + <FormField + control={form.control} + name="responseComment" + render={({ field }) => ( + <FormItem> + <FormLabel>응답 코멘트</FormLabel> + <FormControl> + <Textarea + placeholder="응답에 대한 설명을 입력하세요..." + className="resize-none" + rows={3} + {...field} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + {/* 벤더 코멘트 */} + <FormField + control={form.control} + name="vendorComment" + render={({ field }) => ( + <FormItem> + <FormLabel>벤더 코멘트 (내부용)</FormLabel> + <FormControl> + <Textarea + placeholder="내부 참고용 코멘트를 입력하세요..." + className="resize-none" + rows={2} + {...field} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + {/* 버튼 */} + <div className="flex justify-end gap-2"> + <Button + type="button" + variant="outline" + onClick={handleCancel} + disabled={isUploading} + > + 취소 + </Button> + <Button type="submit" disabled={isUploading || selectedFiles.length === 0}> + {isUploading && <Loader2 className="h-4 w-4 mr-2 animate-spin" />} + {isUploading ? "업로드 중..." : "업로드"} + </Button> + </div> + </form> + </Form> + </DialogContent> + </Dialog> + ); +}
\ No newline at end of file |
