From d689608ca2a54cab2cd12a12f0b6007a1be39ab2 Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Wed, 1 Oct 2025 06:26:44 +0000
Subject: (대표님, 최겸) 구매 견적 첨부파일 type 오류 수정, 문서확정, short
 list 기능 수정
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 lib/rfq-last/attachment/vendor-response-table.tsx | 296 +++++++++++++---------
 1 file changed, 175 insertions(+), 121 deletions(-)

(limited to 'lib/rfq-last/attachment/vendor-response-table.tsx')

diff --git a/lib/rfq-last/attachment/vendor-response-table.tsx b/lib/rfq-last/attachment/vendor-response-table.tsx
index 076fb153..47a23d18 100644
--- a/lib/rfq-last/attachment/vendor-response-table.tsx
+++ b/lib/rfq-last/attachment/vendor-response-table.tsx
@@ -17,7 +17,9 @@ import {
   FileCode,
   Building2,
   Calendar,
-  AlertCircle, X
+  AlertCircle, 
+  X,
+  CheckCircle2
 } from "lucide-react";
 import { format, formatDistanceToNow, isValid, isBefore, isAfter } from "date-fns";
 import { ko } from "date-fns/locale";
@@ -43,7 +45,7 @@ import type {
   DataTableRowAction,
 } from "@/types/table";
 import { cn } from "@/lib/utils";
-import { getRfqVendorAttachments, updateAttachmentTypes } from "@/lib/rfq-last/service";
+import { confirmVendorDocuments, getRfqVendorAttachments, updateAttachmentTypes } from "@/lib/rfq-last/service";
 import { downloadFile } from "@/lib/file-download";
 import { toast } from "sonner";
 import { 
@@ -61,7 +63,16 @@ import {
   DialogHeader,
   DialogTitle,
 } from "@/components/ui/dialog";
-
+import {
+  AlertDialog,
+  AlertDialogAction,
+  AlertDialogCancel,
+  AlertDialogContent,
+  AlertDialogDescription,
+  AlertDialogFooter,
+  AlertDialogHeader,
+  AlertDialogTitle,
+} from "@/components/ui/alert-dialog";
 
 // 타입 정의
 interface VendorAttachment {
@@ -153,53 +164,81 @@ export function VendorResponseTable({
   const [data, setData] = React.useState<VendorAttachment[]>(initialData);
   const [isRefreshing, setIsRefreshing] = React.useState(false);
   const [selectedRows, setSelectedRows] = React.useState<VendorAttachment[]>([]);
-
-
-
   const [isUpdating, setIsUpdating] = React.useState(false);
   const [showTypeDialog, setShowTypeDialog] = React.useState(false);
   const [selectedType, setSelectedType] = React.useState<"구매" | "설계" | "">("");
-
   const [selectedVendor, setSelectedVendor] = React.useState<string | null>(null);
+  const [showConfirmDialog, setShowConfirmDialog] = React.useState(false);
+  const [isConfirming, setIsConfirming] = React.useState(false);
+  const [confirmedVendors, setConfirmedVendors] = React.useState<Set<number>>(new Set());
 
   const filteredData = React.useMemo(() => {
     if (!selectedVendor) return data;
     return data.filter(item => item.vendorName === selectedVendor);
   }, [data, selectedVendor]);
 
+  // 현재 선택된 벤더의 ID 가져오기
+  const selectedVendorId = React.useMemo(() => {
+    if (!selectedVendor) return null;
+    const vendorItem = data.find(item => item.vendorName === selectedVendor);
+    return vendorItem?.vendorId || null;
+  }, [selectedVendor, data]);
 
-
-    // 데이터 새로고침
-    const handleRefresh = React.useCallback(async () => {
-      setIsRefreshing(true);
-      try {
-        const result = await getRfqVendorAttachments(rfqId);
-        if (result.vendorSuccess && result.vendorData) {
-          setData(result.vendorData);
-          toast.success("데이터를 새로고침했습니다.");
-        } else {
-          toast.error("데이터를 불러오는데 실패했습니다.");
-        }
-      } catch (error) {
-        console.error("Refresh error:", error);
-        toast.error("새로고침 중 오류가 발생했습니다.");
-      } finally {
-        setIsRefreshing(false);
+  // 데이터 새로고침
+  const handleRefresh = React.useCallback(async () => {
+    setIsRefreshing(true);
+    try {
+      const result = await getRfqVendorAttachments(rfqId);
+      if (result.vendorSuccess && result.vendorData) {
+        setData(result.vendorData);
+        toast.success("데이터를 새로고침했습니다.");
+      } else {
+        toast.error("데이터를 불러오는데 실패했습니다.");
       }
-    }, [rfqId]);
+    } catch (error) {
+      console.error("Refresh error:", error);
+      toast.error("새로고침 중 오류가 발생했습니다.");
+    } finally {
+      setIsRefreshing(false);
+    }
+  }, [rfqId]);
+
+  const toggleVendorFilter = (vendor: string) => {
+    if (selectedVendor === vendor) {
+      setSelectedVendor(null); // 이미 선택된 벤더를 다시 클릭하면 필터 해제
+    } else {
+      setSelectedVendor(vendor);
+      // 필터 변경 시 선택 초기화 (옵션)
+      setSelectedRows([]);
+    }
+  };
+
+  // 문서 확정 처리
+  const handleConfirmDocuments = React.useCallback(async () => {
+    if (!selectedVendorId || !selectedVendor) return;
 
-    const toggleVendorFilter = (vendor: string) => {
-      if (selectedVendor === vendor) {
-        setSelectedVendor(null); // 이미 선택된 벤더를 다시 클릭하면 필터 해제
+    setIsConfirming(true);
+    try {
+      const result = await confirmVendorDocuments(rfqId, selectedVendorId);
+      
+      if (result.success) {
+        toast.success(result.message);
+        setConfirmedVendors(prev => new Set(prev).add(selectedVendorId));
+        setShowConfirmDialog(false);
+        // 데이터 새로고침
+        await handleRefresh();
       } else {
-        setSelectedVendor(vendor);
-        // 필터 변경 시 선택 초기화 (옵션)
-        setSelectedRows([]);
+        toast.error(result.message);
       }
-    };
+    } catch (error) {
+      toast.error("문서 확정 중 오류가 발생했습니다.");
+    } finally {
+      setIsConfirming(false);
+    }
+  }, [selectedVendorId, selectedVendor, rfqId, handleRefresh]);
 
-   // 문서 유형 일괄 변경
-   const handleBulkTypeChange = React.useCallback(async () => {
+  // 문서 유형 일괄 변경
+  const handleBulkTypeChange = React.useCallback(async () => {
     if (!selectedType || selectedRows.length === 0) return;
 
     setIsUpdating(true);
@@ -225,8 +264,6 @@ export function VendorResponseTable({
     }
   }, [selectedType, selectedRows, handleRefresh]);
 
-
-
   // 액션 처리
   const handleAction = React.useCallback(async (action: DataTableRowAction<VendorAttachment>) => {
     const attachment = action.row.original;
@@ -352,56 +389,6 @@ export function VendorResponseTable({
       },
       size: 300,
     },
-    // {
-    //   accessorKey: "description",
-    //   header: ({ column }) => <ClientDataTableColumnHeaderSimple column={column} title="설명" />,
-    //   cell: ({ row }) => (
-    //     <div className="max-w-[200px] truncate" title={row.original.description || ""}>
-    //       {row.original.description || "-"}
-    //     </div>
-    //   ),
-    //   size: 200,
-    // },
-    // {
-    //   accessorKey: "validTo",
-    //   header: ({ column }) => <ClientDataTableColumnHeaderSimple column={column} title="유효기간" />,
-    //   cell: ({ row }) => {
-    //     const { validFrom, validTo } = row.original;
-    //     const validity = checkValidity(validTo);
-        
-    //     if (!validTo) return <span className="text-muted-foreground">-</span>;
-        
-    //     return (
-    //       <TooltipProvider>
-    //         <Tooltip>
-    //           <TooltipTrigger asChild>
-    //             <div className="flex items-center gap-2">
-    //               {validity === "expired" && (
-    //                 <AlertCircle className="h-4 w-4 text-red-500" />
-    //               )}
-    //               {validity === "expiring-soon" && (
-    //                 <AlertCircle className="h-4 w-4 text-yellow-500" />
-    //               )}
-    //               <span className={cn(
-    //                 "text-sm",
-    //                 validity === "expired" && "text-red-500",
-    //                 validity === "expiring-soon" && "text-yellow-500"
-    //               )}>
-    //                 {format(new Date(validTo), "yyyy-MM-dd")}
-    //               </span>
-    //             </div>
-    //           </TooltipTrigger>
-    //           <TooltipContent>
-    //             <p>유효기간: {validFrom ? format(new Date(validFrom), "yyyy-MM-dd") : "?"} ~ {format(new Date(validTo), "yyyy-MM-dd")}</p>
-    //             {validity === "expired" && <p className="text-red-500">만료됨</p>}
-    //             {validity === "expiring-soon" && <p className="text-yellow-500">곧 만료 예정</p>}
-    //           </TooltipContent>
-    //         </Tooltip>
-    //       </TooltipProvider>
-    //     );
-    //   },
-    //   size: 120,
-    // },
     {
       accessorKey: "responseStatus",
       header: ({ column }) => <ClientDataTableColumnHeaderSimple column={column} title="응답 상태" />,
@@ -496,11 +483,6 @@ export function VendorResponseTable({
       options: [
         { label: "구매", value: "구매" },
         { label: "설계", value: "설계" },
-        // { label: "인증서", value: "인증서" },
-        // { label: "카탈로그", value: "카탈로그" },
-        // { label: "도면", value: "도면" },
-        // { label: "테스트성적서", value: "테스트성적서" },
-        // { label: "기타", value: "기타" },
       ]
     },
     { id: "documentNo", label: "문서번호", type: "text" },
@@ -518,8 +500,6 @@ export function VendorResponseTable({
         { label: "취소", value: "취소" },
       ]
     },
-    // { id: "validFrom", label: "유효시작일", type: "date" },
-    // { id: "validTo", label: "유효종료일", type: "date" },
     { id: "uploadedAt", label: "업로드일", type: "date" },
   ];
 
@@ -579,17 +559,42 @@ export function VendorResponseTable({
           <span className="text-sm font-medium text-muted-foreground">
             벤더별 필터
           </span>
-          {selectedVendor && (
-            <Button
-              variant="ghost"
-              size="sm"
-              onClick={() => setSelectedVendor(null)}
-              className="h-7 px-2 text-xs"
-            >
-              <X className="h-3 w-3 mr-1" />
-              필터 초기화
-            </Button>
-          )}
+          <div className="flex items-center gap-2">
+            {/* 선택된 벤더의 문서 확정 버튼 */}
+            {selectedVendor && selectedVendorId && (
+              <Button
+                variant="default"
+                size="sm"
+                onClick={() => setShowConfirmDialog(true)}
+                className="h-7"
+                disabled={confirmedVendors.has(selectedVendorId)}
+              >
+                {confirmedVendors.has(selectedVendorId) ? (
+                  <>
+                    <CheckCircle2 className="h-3 w-3 mr-1" />
+                    확정완료
+                  </>
+                ) : (
+                  <>
+                    <CheckCircle2 className="h-3 w-3 mr-1" />
+                    {selectedVendor} 문서 확정
+                  </>
+                )}
+              </Button>
+            )}
+            
+            {selectedVendor && (
+              <Button
+                variant="ghost"
+                size="sm"
+                onClick={() => setSelectedVendor(null)}
+                className="h-7 px-2 text-xs"
+              >
+                <X className="h-3 w-3 mr-1" />
+                필터 초기화
+              </Button>
+            )}
+          </div>
         </div>
         
         {/* 벤더 버튼들 */}
@@ -607,20 +612,29 @@ export function VendorResponseTable({
           </Button>
           
           {/* 각 벤더별 버튼 */}
-          {Array.from(vendorCounts.entries()).map(([vendor, count]) => (
-            <Button
-              key={vendor}
-              variant={selectedVendor === vendor ? "default" : "outline"}
-              size="sm"
-              onClick={() => toggleVendorFilter(vendor)}
-              className="h-7"
-            >
-              <Building2 className="h-3 w-3 mr-1" />
-              <span className="text-xs">
-                {vendor} ({count})
-              </span>
-            </Button>
-          ))}
+          {Array.from(vendorCounts.entries()).map(([vendor, count]) => {
+            const vendorItem = data.find(item => item.vendorName === vendor);
+            const vendorId = vendorItem?.vendorId;
+            const isConfirmed = vendorId ? confirmedVendors.has(vendorId) : false;
+            
+            return (
+              <Button
+                key={vendor}
+                variant={selectedVendor === vendor ? "default" : "outline"}
+                size="sm"
+                onClick={() => toggleVendorFilter(vendor)}
+                className="h-7 relative"
+              >
+                {isConfirmed && (
+                  <CheckCircle2 className="h-3 w-3 mr-1 text-green-600" />
+                )}
+                <Building2 className="h-3 w-3 mr-1" />
+                <span className="text-xs">
+                  {vendor} ({count})
+                </span>
+              </Button>
+            );
+          })}
         </div>
 
         {/* 현재 필터 상태 표시 */}
@@ -636,7 +650,7 @@ export function VendorResponseTable({
 
       <ClientDataTable
         columns={columns}
-        data={filteredData} // 필터링된 데이터 사용
+        data={filteredData}
         advancedFilterFields={advancedFilterFields}
         autoSizeColumns={true}
         compact={true}
@@ -650,8 +664,8 @@ export function VendorResponseTable({
         {additionalActions}
       </ClientDataTable>
 
-       {/* 문서 유형 변경 다이얼로그 */}
-       <Dialog open={showTypeDialog} onOpenChange={setShowTypeDialog}>
+      {/* 문서 유형 변경 다이얼로그 */}
+      <Dialog open={showTypeDialog} onOpenChange={setShowTypeDialog}>
         <DialogContent className="sm:max-w-[425px]">
           <DialogHeader>
             <DialogTitle>문서 유형 변경</DialogTitle>
@@ -724,6 +738,46 @@ export function VendorResponseTable({
           </DialogFooter>
         </DialogContent>
       </Dialog>
+
+      {/* 문서 확정 확인 다이얼로그 */}
+      <AlertDialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
+        <AlertDialogContent>
+          <AlertDialogHeader>
+            <AlertDialogTitle>문서 확정 확인</AlertDialogTitle>
+            <AlertDialogDescription className="space-y-2">
+              <p>
+                <span className="font-semibold">{selectedVendor}</span> 벤더의 모든 문서를 확정하시겠습니까?
+              </p>
+              <p className="text-sm text-muted-foreground">
+                이 작업은 해당 벤더의 모든 응답 문서를 확정 처리합니다.
+              </p>
+              <p className="text-sm text-yellow-600">
+                ⚠️ 확정 후에는 되돌릴 수 없습니다.
+              </p>
+            </AlertDialogDescription>
+          </AlertDialogHeader>
+          <AlertDialogFooter>
+            <AlertDialogCancel disabled={isConfirming}>취소</AlertDialogCancel>
+            <AlertDialogAction
+              onClick={handleConfirmDocuments}
+              disabled={isConfirming}
+              className="bg-blue-600 hover:bg-blue-700"
+            >
+              {isConfirming ? (
+                <>
+                  <RefreshCw className="mr-2 h-4 w-4 animate-spin" />
+                  확정 중...
+                </>
+              ) : (
+                <>
+                  <CheckCircle2 className="mr-2 h-4 w-4" />
+                  확정
+                </>
+              )}
+            </AlertDialogAction>
+          </AlertDialogFooter>
+        </AlertDialogContent>
+      </AlertDialog>
     </div>
   );
 }
\ No newline at end of file
-- 
cgit v1.2.3

