From ba8cd44a0ed2c613a5f2cee06bfc9bd0f61f21c7 Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Fri, 7 Nov 2025 08:39:04 +0000
Subject: (최겸) 입찰/견적 수정사항
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 lib/bidding/list/bidding-detail-dialogs.tsx | 554 ++++++++--------------------
 1 file changed, 149 insertions(+), 405 deletions(-)

(limited to 'lib/bidding/list/bidding-detail-dialogs.tsx')

diff --git a/lib/bidding/list/bidding-detail-dialogs.tsx b/lib/bidding/list/bidding-detail-dialogs.tsx
index 4fbca616..065000ce 100644
--- a/lib/bidding/list/bidding-detail-dialogs.tsx
+++ b/lib/bidding/list/bidding-detail-dialogs.tsx
@@ -9,58 +9,49 @@ import {
     DialogHeader,
     DialogTitle,
 } from "@/components/ui/dialog"
-import {
-    Table,
-    TableBody,
-    TableCell,
-    TableHead,
-    TableHeader,
-    TableRow,
-} from "@/components/ui/table"
 import { Button } from "@/components/ui/button"
 import { Badge } from "@/components/ui/badge"
 import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
-import { Separator } from "@/components/ui/separator"
 import { ScrollArea } from "@/components/ui/scroll-area"
+import { Textarea } from "@/components/ui/textarea"
+import { Label } from "@/components/ui/label"
+import { Input } from "@/components/ui/input"
 import {
     Tooltip,
     TooltipContent,
     TooltipProvider,
     TooltipTrigger,
 } from "@/components/ui/tooltip"
-import { 
-    CalendarIcon, 
-    ClockIcon, 
+import {
+    CalendarIcon,
+    ClockIcon,
     MapPinIcon,
     FileTextIcon,
-    DownloadIcon,
-    EyeIcon,
-    PackageIcon,
-    HashIcon,
-    DollarSignIcon,
-    WeightIcon,
-    ExternalLinkIcon
+    ExternalLinkIcon,
+    FileXIcon,
+    UploadIcon
 } from "lucide-react"
-import { toast } from "sonner"
 import { BiddingListItem } from "@/db/schema"
 import { downloadFile, formatFileSize, getFileInfo } from "@/lib/file-download"
-import { getPRDetailsAction, getSpecificationMeetingDetailsAction } from "../service"
+import { getSpecificationMeetingDetailsAction } from "../service"
+import { bidClosureAction } from "../actions"
 import { formatDate } from "@/lib/utils"
+import { toast } from "sonner"
 
 // 타입 정의
 interface SpecificationMeetingDetails {
     id: number;
     biddingId: number;
     meetingDate: string;
-    meetingTime: string | null;
-    location: string | null;
-    address: string | null;
-    contactPerson: string | null;
-    contactPhone: string | null;
-    contactEmail: string | null;
-    agenda: string | null;
-    materials: string | null;
-    notes: string | null;
+    meetingTime?: string | null;
+    location?: string | null;
+    address?: string | null;
+    contactPerson?: string | null;
+    contactPhone?: string | null;
+    contactEmail?: string | null;
+    agenda?: string | null;
+    materials?: string | null;
+    notes?: string | null;
     isRequired: boolean;
     createdAt: string;
     updatedAt: string;
@@ -70,60 +61,13 @@ interface SpecificationMeetingDetails {
         originalFileName: string;
         fileSize: number;
         filePath: string;
-        title: string | null;
+        title?: string | null;
         uploadedAt: string;
-        uploadedBy: string | null;
-    }>;
-}
-
-interface PRDetails {
-    documents: Array<{
-        id: number;
-        documentName: string;
-        fileName: string;
-        originalFileName: string;
-        fileSize: number;
-        filePath: string;
-        registeredAt: string;
-        registeredBy: string | null;
-        version: string | null;
-        description: string | null;
-        createdAt: string;
-        updatedAt: string;
-    }>;
-    items: Array<{
-        id: number;
-        itemNumber: string;
-        itemInfo: string | null;
-        quantity: number | null;
-        quantityUnit: string | null;
-        requestedDeliveryDate: string | null;
-        prNumber: string | null;
-        annualUnitPrice: number | null;
-        currency: string | null;
-        totalWeight: number | null;
-        weightUnit: string | null;
-        materialDescription: string | null;
-        hasSpecDocument: boolean;
-        createdAt: string;
-        updatedAt: string;
-        specDocuments: Array<{
-            id: number;
-            fileName: string;
-            originalFileName: string;
-            fileSize: number;
-            filePath: string;
-            uploadedAt: string;
-            title: string | null;
-        }>;
+        uploadedBy?: string | null;
     }>;
 }
 
-interface ActionResult<T> {
-    success: boolean;
-    data?: T;
-    error?: string;
-}
+// PR 관련 타입과 컴포넌트는 bidding-pr-documents-dialog.tsx로 이동됨
 
 // 파일 다운로드 훅
 const useFileDownload = () => {
@@ -212,52 +156,6 @@ const FileDownloadLink: React.FC<FileDownloadLinkProps> = ({
     );
 };
 
-// 파일 다운로드 버튼 컴포넌트 (간소화된 버전)
-interface FileDownloadButtonProps {
-    filePath: string;
-    fileName: string;
-    fileSize?: number;
-    title?: string | null;
-    variant?: "download" | "preview";
-    size?: "sm" | "default" | "lg";
-}
-
-const FileDownloadButton: React.FC<FileDownloadButtonProps> = ({
-    filePath,
-    fileName,
-    fileSize,
-    title,
-    variant = "download",
-    size = "sm"
-}) => {
-    const { handleDownload, downloadingFiles } = useFileDownload();
-    const fileInfo = getFileInfo(fileName);
-    const fileKey = `${filePath}_${fileName}`;
-    const isDownloading = downloadingFiles.has(fileKey);
-
-    const Icon = variant === "preview" && fileInfo.canPreview ? EyeIcon : DownloadIcon;
-
-    return (
-        <Button
-            onClick={() => handleDownload(filePath, fileName, { action: variant })}
-            disabled={isDownloading}
-            size={size}
-            variant="outline"
-            className="gap-2"
-        >
-            <Icon className="h-4 w-4" />
-            {isDownloading ? "처리중..." : (
-                variant === "preview" && fileInfo.canPreview ? "미리보기" : "다운로드"
-            )}
-            {fileSize && size !== "sm" && (
-                <span className="text-xs text-muted-foreground">
-                    ({formatFileSize(fileSize)})
-                </span>
-            )}
-        </Button>
-    );
-};
-
 // 사양설명회 다이얼로그
 interface SpecificationMeetingDialogProps {
     open: boolean;
@@ -458,285 +356,131 @@ export function SpecificationMeetingDialog({
     );
 }
 
-// PR 문서 다이얼로그
-interface PrDocumentsDialogProps {
-    open: boolean;
-    onOpenChange: (open: boolean) => void;
-    bidding: BiddingListItem | null;
-}
-
-export function PrDocumentsDialog({
-    open,
-    onOpenChange,
-    bidding
-}: PrDocumentsDialogProps) {
-    const [data, setData] = useState<PRDetails | null>(null);
-    const [loading, setLoading] = useState(false);
-    const [error, setError] = useState<string | null>(null);
-
-    useEffect(() => {
-        if (open && bidding) {
-            fetchPRData();
-        }
-    }, [open, bidding]);
-
-    const fetchPRData = async () => {
-        if (!bidding) return;
-
-        setLoading(true);
-        setError(null);
-        
-        try {
-            const result = await getPRDetailsAction(bidding.id);
-            
-            if (result.success && result.data) {
-                setData(result.data);
-            } else {
-                setError(result.error || "PR 문서 정보를 불러올 수 없습니다.");
-            }
-        } catch (err) {
-            setError("데이터 로딩 중 오류가 발생했습니다.");
-            console.error("Failed to fetch PR data:", err);
-        } finally {
-            setLoading(false);
-        }
-    };
-
-    const formatCurrency = (amount: number | null, currency: string | null) => {
-        if (amount === null) return "-";
-        return `${amount.toLocaleString()} ${currency || ""}`;
-    };
-
-    const formatWeight = (weight: number | null, unit: string | null) => {
-        if (weight === null) return "-";
-        return `${weight.toLocaleString()} ${unit || ""}`;
-    };
+// PR 문서 다이얼로그는 bidding-pr-documents-dialog.tsx로 이동됨
+// import { PrDocumentsDialog } from './bidding-pr-documents-dialog'로 사용하세요
 
-    return (
-        <Dialog open={open} onOpenChange={onOpenChange}>
-            <DialogContent className="max-w-7xl max-h-[90vh]">
-                <DialogHeader>
-                    <DialogTitle className="flex items-center gap-2">
-                        <PackageIcon className="h-5 w-5" />
-                        PR 문서
-                    </DialogTitle>
-                    <DialogDescription>
-                        {bidding?.title}의 PR 문서 및 아이템 정보입니다.
-                    </DialogDescription>
-                </DialogHeader>
-
-                <ScrollArea className="max-h-[75vh]">
-                    {loading ? (
-                        <div className="flex items-center justify-center py-8">
-                            <div className="text-center">
-                                <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-2"></div>
-                                <p className="text-sm text-muted-foreground">로딩 중...</p>
-                            </div>
-                        </div>
-                    ) : error ? (
-                        <div className="flex items-center justify-center py-8">
-                            <div className="text-center">
-                                <p className="text-sm text-destructive mb-2">{error}</p>
-                                <Button onClick={fetchPRData} size="sm">
-                                    다시 시도
-                                </Button>
-                            </div>
-                        </div>
-                    ) : data ? (
-                        <div className="space-y-6">
-                            {/* PR 문서 목록 */}
-                            {data.documents.length > 0 && (
-                                <Card>
-                                    <CardHeader>
-                                        <CardTitle className="text-lg flex items-center gap-2">
-                                            <FileTextIcon className="h-5 w-5" />
-                                            PR 문서 ({data.documents.length}개)
-                                        </CardTitle>
-                                    </CardHeader>
-                                    <CardContent>
-                                        <Table>
-                                            <TableHeader>
-                                                <TableRow>
-                                                    <TableHead>문서명</TableHead>
-                                                    <TableHead>파일명</TableHead>
-                                                    <TableHead>버전</TableHead>
-                                                    <TableHead>크기</TableHead>
-                                                    <TableHead>등록일</TableHead>
-                                                    <TableHead>등록자</TableHead>
-                                                    <TableHead className="text-right">다운로드</TableHead>
-                                                </TableRow>
-                                            </TableHeader>
-                                            <TableBody>
-                                                {data.documents.map((doc) => (
-                                                    <TableRow key={doc.id}>
-                                                        <TableCell className="font-medium">
-                                                            {doc.documentName}
-                                                            {doc.description && (
-                                                                <div className="text-xs text-muted-foreground mt-1">
-                                                                    {doc.description}
-                                                                </div>
-                                                            )}
-                                                        </TableCell>
-                                                        <TableCell>
-                                                            <FileDownloadLink
-                                                                filePath={doc.filePath}
-                                                                fileName={doc.originalFileName}
-                                                                fileSize={doc.fileSize}
-                                                            />
-                                                        </TableCell>
-                                                        <TableCell>
-                                                            {doc.version ? (
-                                                                <Badge variant="outline">{doc.version}</Badge>
-                                                            ) : "-"}
-                                                        </TableCell>
-                                                        <TableCell>{formatFileSize(doc.fileSize)}</TableCell>
-                                                        <TableCell>
-                                                            {new Date(doc.registeredAt).toLocaleDateString('ko-KR')}
-                                                        </TableCell>
-                                                        <TableCell>{doc.registeredBy || "-"}</TableCell>
-                                                        <TableCell className="text-right">
-                                                            <FileDownloadButton
-                                                                filePath={doc.filePath}
-                                                                fileName={doc.originalFileName}
-                                                                fileSize={doc.fileSize}
-                                                                variant="download"
-                                                                size="sm"
-                                                            />
-                                                        </TableCell>
-                                                    </TableRow>
-                                                ))}
-                                            </TableBody>
-                                        </Table>
-                                    </CardContent>
-                                </Card>
-                            )}
+// 폐찰하기 다이얼로그
+interface BidClosureDialogProps {
+  open: boolean;
+  onOpenChange: (open: boolean) => void;
+  bidding: BiddingListItem | null;
+  userId: string;
+}
 
-                            {/* PR 아이템 테이블 */}
-                            {data.items.length > 0 && (
-                                <Card>
-                                    <CardHeader>
-                                        <CardTitle className="text-lg flex items-center gap-2">
-                                            <HashIcon className="h-5 w-5" />
-                                            PR 아이템 ({data.items.length}개)
-                                        </CardTitle>
-                                    </CardHeader>
-                                    <CardContent>
-                                        <Table>
-                                            <TableHeader>
-                                                <TableRow>
-                                                    <TableHead className="w-[100px]">아이템 번호</TableHead>
-                                                    <TableHead className="w-[150px]">PR 번호</TableHead>
-                                                    <TableHead>아이템 정보</TableHead>
-                                                    <TableHead className="w-[120px]">수량</TableHead>
-                                                    <TableHead className="w-[120px]">단가</TableHead>
-                                                    <TableHead className="w-[120px]">중량</TableHead>
-                                                    <TableHead className="w-[120px]">요청 납기</TableHead>
-                                                    <TableHead className="w-[200px]">스펙 문서</TableHead>
-                                                </TableRow>
-                                            </TableHeader>
-                                            <TableBody>
-                                                {data.items.map((item) => (
-                                                    <TableRow key={item.id}>
-                                                        <TableCell className="font-medium">
-                                                            {item.itemNumber}
-                                                        </TableCell>
-                                                        <TableCell>
-                                                            {item.prNumber || "-"}
-                                                        </TableCell>
-                                                        <TableCell>
-                                                            <div>
-                                                                {item.itemInfo && (
-                                                                    <div className="font-medium text-sm mb-1">{item.itemInfo}</div>
-                                                                )}
-                                                                {item.materialDescription && (
-                                                                    <div className="text-xs text-muted-foreground">
-                                                                        {item.materialDescription}
-                                                                    </div>
-                                                                )}
-                                                            </div>
-                                                        </TableCell>
-                                                        <TableCell>
-                                                            <div className="flex items-center gap-1">
-                                                                <PackageIcon className="h-3 w-3 text-muted-foreground" />
-                                                                <span className="text-sm">
-                                                                    {item.quantity ? `${item.quantity.toLocaleString()} ${item.quantityUnit || ""}` : "-"}
-                                                                </span>
-                                                            </div>
-                                                        </TableCell>
-                                                        <TableCell>
-                                                            <div className="flex items-center gap-1">
-                                                                <DollarSignIcon className="h-3 w-3 text-muted-foreground" />
-                                                                <span className="text-sm">
-                                                                    {formatCurrency(item.annualUnitPrice, item.currency)}
-                                                                </span>
-                                                            </div>
-                                                        </TableCell>
-                                                        <TableCell>
-                                                            <div className="flex items-center gap-1">
-                                                                <WeightIcon className="h-3 w-3 text-muted-foreground" />
-                                                                <span className="text-sm">
-                                                                    {formatWeight(item.totalWeight, item.weightUnit)}
-                                                                </span>
-                                                            </div>
-                                                        </TableCell>
-                                                        <TableCell>
-                                                            {item.requestedDeliveryDate ? (
-                                                                <div className="flex items-center gap-1">
-                                                                    <CalendarIcon className="h-3 w-3 text-muted-foreground" />
-                                                                    <span className="text-sm">
-                                                                        {new Date(item.requestedDeliveryDate).toLocaleDateString('ko-KR')}
-                                                                    </span>
-                                                                </div>
-                                                            ) : "-"}
-                                                        </TableCell>
-                                                        <TableCell>
-                                                            <div className="space-y-1">
-                                                                <div className="flex items-center gap-2">
-                                                                    <Badge variant={item.hasSpecDocument ? "default" : "secondary"} className="text-xs">
-                                                                        {item.hasSpecDocument ? "있음" : "없음"}
-                                                                    </Badge>
-                                                                    {item.specDocuments.length > 0 && (
-                                                                        <span className="text-xs text-muted-foreground">
-                                                                            ({item.specDocuments.length}개)
-                                                                        </span>
-                                                                    )}
-                                                                </div>
-                                                                {item.specDocuments.length > 0 && (
-                                                                    <div className="space-y-1">
-                                                                        {item.specDocuments.map((doc, index) => (
-                                                                            <div key={doc.id} className="text-xs">
-                                                                                <FileDownloadLink
-                                                                                    filePath={doc.filePath}
-                                                                                    fileName={doc.originalFileName}
-                                                                                    fileSize={doc.fileSize}
-                                                                                    title={doc.title}
-                                                                                    className="text-xs"
-                                                                                />
-                                                                            </div>
-                                                                        ))}
-                                                                    </div>
-                                                                )}
-                                                            </div>
-                                                        </TableCell>
-                                                    </TableRow>
-                                                ))}
-                                            </TableBody>
-                                        </Table>
-                                    </CardContent>
-                                </Card>
-                            )}
+export function BidClosureDialog({
+  open,
+  onOpenChange,
+  bidding,
+  userId
+}: BidClosureDialogProps) {
+  const [description, setDescription] = useState('')
+  const [files, setFiles] = useState<File[]>([])
+  const [isSubmitting, setIsSubmitting] = useState(false)
+
+  const handleSubmit = async (e: React.FormEvent) => {
+    e.preventDefault()
+
+    if (!bidding || !description.trim()) {
+      toast.error('폐찰 사유를 입력해주세요.')
+      return
+    }
+
+    setIsSubmitting(true)
+
+    try {
+      const result = await bidClosureAction(bidding.id, {
+        description: description.trim(),
+        files
+      }, userId)
+
+      if (result.success) {
+        toast.success(result.message)
+        onOpenChange(false)
+        // 페이지 새로고침 또는 상태 업데이트
+        window.location.reload()
+      } else {
+        toast.error(result.error || '폐찰 처리 중 오류가 발생했습니다.')
+      }
+    } catch (error) {
+      toast.error('폐찰 처리 중 오류가 발생했습니다.')
+    } finally {
+      setIsSubmitting(false)
+    }
+  }
+
+  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+    if (e.target.files) {
+      setFiles(Array.from(e.target.files))
+    }
+  }
+
+  if (!bidding) return null
+
+  return (
+    <Dialog open={open} onOpenChange={onOpenChange}>
+      <DialogContent className="max-w-2xl">
+        <DialogHeader>
+          <DialogTitle className="flex items-center gap-2">
+            <FileXIcon className="h-5 w-5 text-destructive" />
+            폐찰하기
+          </DialogTitle>
+          <DialogDescription>
+            {bidding.title} ({bidding.biddingNumber})를 폐찰합니다.
+          </DialogDescription>
+        </DialogHeader>
+
+        <form onSubmit={handleSubmit} className="space-y-4">
+          <div className="space-y-2">
+            <Label htmlFor="description">폐찰 사유 <span className="text-destructive">*</span></Label>
+            <Textarea
+              id="description"
+              placeholder="폐찰 사유를 입력해주세요..."
+              value={description}
+              onChange={(e) => setDescription(e.target.value)}
+              className="min-h-[100px]"
+              required
+            />
+          </div>
+
+          <div className="space-y-2">
+            <Label htmlFor="files">첨부파일</Label>
+            <Input
+              id="files"
+              type="file"
+              multiple
+              onChange={handleFileChange}
+              className="cursor-pointer"
+              accept=".pdf,.doc,.docx,.xls,.xlsx,.txt,.jpg,.jpeg,.png"
+            />
+            {files.length > 0 && (
+              <div className="text-sm text-muted-foreground">
+                선택된 파일: {files.map(f => f.name).join(', ')}
+              </div>
+            )}
+          </div>
+
+          <div className="flex justify-end gap-2 pt-4">
+            <Button
+              type="button"
+              variant="outline"
+              onClick={() => onOpenChange(false)}
+              disabled={isSubmitting}
+            >
+              취소
+            </Button>
+            <Button
+              type="submit"
+              variant="destructive"
+              disabled={isSubmitting || !description.trim()}
+            >
+              {isSubmitting ? '처리 중...' : '폐찰하기'}
+            </Button>
+          </div>
+        </form>
+      </DialogContent>
+    </Dialog>
+  )
+}
 
-                            {/* 데이터가 없는 경우 */}
-                            {data.documents.length === 0 && data.items.length === 0 && (
-                                <div className="text-center py-8">
-                                    <FileTextIcon className="h-12 w-12 text-muted-foreground mx-auto mb-2" />
-                                    <p className="text-muted-foreground">PR 문서가 없습니다.</p>
-                                </div>
-                            )}
-                        </div>
-                    ) : null}
-                </ScrollArea>
-            </DialogContent>
-        </Dialog>
-    );
-}
\ No newline at end of file
+// Re-export for backward compatibility
+export { PrDocumentsDialog } from './bidding-pr-documents-dialog'
\ No newline at end of file
-- 
cgit v1.2.3

