From 47fb72704161b4b58a27c7f5c679fc44618de9a1 Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Tue, 4 Nov 2025 10:03:32 +0000
Subject: (최겸) 구매 견적 내 RFQ Cancel/Delete, 연동제 적용, MRC Type 개발
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../vendor/cancel-vendor-response-dialog.tsx       | 208 +++++++++++++++++++++
 1 file changed, 208 insertions(+)
 create mode 100644 lib/rfq-last/vendor/cancel-vendor-response-dialog.tsx

(limited to 'lib/rfq-last/vendor/cancel-vendor-response-dialog.tsx')

diff --git a/lib/rfq-last/vendor/cancel-vendor-response-dialog.tsx b/lib/rfq-last/vendor/cancel-vendor-response-dialog.tsx
new file mode 100644
index 00000000..414cfa4b
--- /dev/null
+++ b/lib/rfq-last/vendor/cancel-vendor-response-dialog.tsx
@@ -0,0 +1,208 @@
+"use client";
+
+import * as React from "react";
+import {
+  AlertDialog,
+  AlertDialogAction,
+  AlertDialogCancel,
+  AlertDialogContent,
+  AlertDialogDescription,
+  AlertDialogFooter,
+  AlertDialogHeader,
+  AlertDialogTitle,
+} from "@/components/ui/alert-dialog";
+import { Label } from "@/components/ui/label";
+import { Textarea } from "@/components/ui/textarea";
+import { cancelVendorResponse } from "@/lib/rfq-last/cancel-vendor-response-action";
+import { Loader2, AlertTriangle } from "lucide-react";
+import { Alert, AlertDescription } from "@/components/ui/alert";
+
+interface CancelVendorResponseDialogProps {
+  open: boolean;
+  onOpenChange: (open: boolean) => void;
+  rfqId: number;
+  selectedVendors: Array<{
+    detailId: number;
+    vendorId: number;
+    vendorName: string;
+    vendorCode?: string | null;
+  }>;
+  onSuccess?: () => void;
+}
+
+export function CancelVendorResponseDialog({
+  open,
+  onOpenChange,
+  rfqId,
+  selectedVendors,
+  onSuccess,
+}: CancelVendorResponseDialogProps) {
+  const [isCancelling, setIsCancelling] = React.useState(false);
+  const [cancelReason, setCancelReason] = React.useState("");
+  const [error, setError] = React.useState<string | null>(null);
+  const [results, setResults] = React.useState<Array<{ detailId: number; success: boolean; error?: string }> | undefined>();
+
+  const handleCancel = async () => {
+    if (!cancelReason || cancelReason.trim() === "") {
+      setError("취소 사유를 입력해주세요.");
+      return;
+    }
+
+    setIsCancelling(true);
+    setError(null);
+    setResults(undefined);
+
+    try {
+      const detailIds = selectedVendors.map(v => v.detailId);
+      const result = await cancelVendorResponse(rfqId, detailIds, cancelReason.trim());
+
+      if (result.results) {
+        setResults(result.results);
+      }
+
+      if (result.success) {
+        // 성공 시 다이얼로그 닫기 및 콜백 호출
+        setTimeout(() => {
+          setCancelReason("");
+          onOpenChange(false);
+          onSuccess?.();
+        }, 1500);
+      } else {
+        setError(result.message);
+      }
+    } catch (err) {
+      setError(err instanceof Error ? err.message : "RFQ 취소 중 오류가 발생했습니다.");
+    } finally {
+      setIsCancelling(false);
+    }
+  };
+
+  const handleClose = () => {
+    if (!isCancelling) {
+      setError(null);
+      setResults(undefined);
+      setCancelReason("");
+      onOpenChange(false);
+    }
+  };
+
+  return (
+    <AlertDialog open={open} onOpenChange={handleClose}>
+      <AlertDialogContent className="max-w-2xl">
+        <AlertDialogHeader>
+          <AlertDialogTitle>RFQ 취소</AlertDialogTitle>
+          <AlertDialogDescription className="space-y-4">
+            <div>
+              선택된 벤더에 대한 RFQ를 취소합니다. 취소 후 해당 벤더는 더 이상 견적을 제출할 수 없습니다.
+            </div>
+
+            {/* 취소 대상 벤더 목록 */}
+            {selectedVendors.length > 0 && (
+              <div className="space-y-2">
+                <p className="font-medium text-sm">취소 대상 벤더 ({selectedVendors.length}건):</p>
+                <div className="max-h-40 overflow-y-auto border rounded-md p-3 space-y-1">
+                  {selectedVendors.map((vendor) => (
+                    <div key={vendor.detailId} className="text-sm">
+                      <span className="font-medium">{vendor.vendorName}</span>
+                      {vendor.vendorCode && (
+                        <span className="text-muted-foreground ml-2">
+                          ({vendor.vendorCode})
+                        </span>
+                      )}
+                    </div>
+                  ))}
+                </div>
+              </div>
+            )}
+
+            {/* 취소 사유 입력 */}
+            <div className="space-y-2">
+              <Label htmlFor="cancelReason">취소 사유 *</Label>
+              <Textarea
+                id="cancelReason"
+                placeholder="RFQ 취소 사유를 입력해주세요..."
+                value={cancelReason}
+                onChange={(e) => setCancelReason(e.target.value)}
+                disabled={isCancelling || !!results}
+                rows={4}
+                className="resize-none"
+              />
+            </div>
+
+            {/* 진행 중 상태 */}
+            {isCancelling && (
+              <div className="flex items-center gap-2 text-sm text-muted-foreground">
+                <Loader2 className="h-4 w-4 animate-spin" />
+                <span>RFQ 취소 처리 중...</span>
+              </div>
+            )}
+
+            {/* 결과 표시 */}
+            {results && !isCancelling && (
+              <div className="space-y-2">
+                <p className="font-medium text-sm">처리 결과:</p>
+                <div className="max-h-40 overflow-y-auto border rounded-md p-3 space-y-2">
+                  {results.map((result) => {
+                    const vendor = selectedVendors.find(v => v.detailId === result.detailId);
+                    return (
+                      <div
+                        key={result.detailId}
+                        className={`text-sm ${
+                          result.success ? "text-green-600" : "text-red-600"
+                        }`}
+                      >
+                        <span className="font-medium">
+                          {vendor?.vendorName || `Detail ID: ${result.detailId}`}
+                        </span>
+                        {result.success ? (
+                          <span className="ml-2">✅ 취소 완료</span>
+                        ) : (
+                          <span className="ml-2">
+                            ❌ 실패: {result.error || "알 수 없는 오류"}
+                          </span>
+                        )}
+                      </div>
+                    );
+                  })}
+                </div>
+              </div>
+            )}
+
+            {/* 오류 메시지 */}
+            {error && !isCancelling && (
+              <Alert variant="destructive">
+                <AlertTriangle className="h-4 w-4" />
+                <AlertDescription>{error}</AlertDescription>
+              </Alert>
+            )}
+          </AlertDialogDescription>
+        </AlertDialogHeader>
+        <AlertDialogFooter>
+          <AlertDialogCancel disabled={isCancelling}>취소</AlertDialogCancel>
+          {!results && (
+            <AlertDialogAction
+              onClick={handleCancel}
+              disabled={isCancelling || !cancelReason.trim()}
+              className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
+            >
+              {isCancelling ? (
+                <>
+                  <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+                  취소 중...
+                </>
+              ) : (
+                "RFQ 취소"
+              )}
+            </AlertDialogAction>
+          )}
+          {results && (
+            <AlertDialogAction onClick={handleClose}>
+              닫기
+            </AlertDialogAction>
+          )}
+        </AlertDialogFooter>
+      </AlertDialogContent>
+    </AlertDialog>
+  );
+}
+
-- 
cgit v1.2.3

