summaryrefslogtreecommitdiff
path: root/lib/itb/table/edit-purchase-request-sheet.tsx
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-09-18 00:23:40 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-09-18 00:23:40 +0000
commitcf8dac0c6490469dab88a560004b0c07dbd48612 (patch)
treeb9e76061e80d868331e6b4277deecb9086f845f3 /lib/itb/table/edit-purchase-request-sheet.tsx
parente5745fc0268bbb5770bc14a55fd58a0ec30b466e (diff)
(대표님) rfq, 계약, 서명 등
Diffstat (limited to 'lib/itb/table/edit-purchase-request-sheet.tsx')
-rw-r--r--lib/itb/table/edit-purchase-request-sheet.tsx1081
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