summaryrefslogtreecommitdiff
path: root/lib/b-rfq/vendor-response/upload-response-dialog.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/b-rfq/vendor-response/upload-response-dialog.tsx')
-rw-r--r--lib/b-rfq/vendor-response/upload-response-dialog.tsx325
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