"use client" import * as React from "react" import { useState, useEffect } from "react" import { Dialog, DialogContent, DialogDescription, 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 { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip" import { CalendarIcon, ClockIcon, MapPinIcon, FileTextIcon, DownloadIcon, EyeIcon, PackageIcon, HashIcon, DollarSignIcon, WeightIcon, ExternalLinkIcon } 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 { formatDate } from "@/lib/utils" // 타입 정의 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; isRequired: boolean; createdAt: string; updatedAt: string; documents: Array<{ id: number; fileName: string; originalFileName: string; fileSize: number; filePath: string; 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; }>; }>; } interface ActionResult { success: boolean; data?: T; error?: string; } // 파일 다운로드 훅 const useFileDownload = () => { const [downloadingFiles, setDownloadingFiles] = useState>(new Set()); const handleDownload = async (filePath: string, fileName: string, options?: { action?: 'download' | 'preview' }) => { const fileKey = `${filePath}_${fileName}`; if (downloadingFiles.has(fileKey)) return; setDownloadingFiles(prev => new Set(prev).add(fileKey)); try { await downloadFile(filePath, fileName, { action: options?.action || 'download', showToast: true, showSuccessToast: true, onError: (error) => { console.error("파일 다운로드 실패:", error); }, onSuccess: (fileName, fileSize) => { console.log("파일 다운로드 성공:", fileName, fileSize ? formatFileSize(fileSize) : ''); } }); } catch (error) { console.error("다운로드 처리 중 오류:", error); } finally { setDownloadingFiles(prev => { const newSet = new Set(prev); newSet.delete(fileKey); return newSet; }); } }; return { handleDownload, downloadingFiles }; }; // 파일 링크 컴포넌트 interface FileDownloadLinkProps { filePath: string; fileName: string; fileSize?: number; title?: string | null; className?: string; } const FileDownloadLink: React.FC = ({ filePath, fileName, fileSize, title, className = "" }) => { const { handleDownload, downloadingFiles } = useFileDownload(); const fileInfo = getFileInfo(fileName); const fileKey = `${filePath}_${fileName}`; const isDownloading = downloadingFiles.has(fileKey); return (
{fileName}
{fileSize &&
{formatFileSize(fileSize)}
}
클릭하여 다운로드
); }; // 파일 다운로드 버튼 컴포넌트 (간소화된 버전) interface FileDownloadButtonProps { filePath: string; fileName: string; fileSize?: number; title?: string | null; variant?: "download" | "preview"; size?: "sm" | "default" | "lg"; } const FileDownloadButton: React.FC = ({ 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 ( ); }; // 사양설명회 다이얼로그 interface SpecificationMeetingDialogProps { open: boolean; onOpenChange: (open: boolean) => void; bidding: BiddingListItem | null; } export function SpecificationMeetingDialog({ open, onOpenChange, bidding }: SpecificationMeetingDialogProps) { const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); useEffect(() => { if (open && bidding) { fetchSpecificationMeetingData(); } }, [open, bidding]); const fetchSpecificationMeetingData = async () => { if (!bidding) return; setLoading(true); setError(null); try { const result = await getSpecificationMeetingDetailsAction(bidding.id); if (result.success && result.data) { setData(result.data); } else { setError(result.error || "사양설명회 정보를 불러올 수 없습니다."); } } catch (err) { setError("데이터 로딩 중 오류가 발생했습니다."); console.error("Failed to fetch specification meeting data:", err); } finally { setLoading(false); } }; return ( 사양설명회 정보 {bidding?.title}의 사양설명회 상세 정보입니다. {loading ? (

로딩 중...

) : error ? (

{error}

) : data ? (
{/* 기본 정보 */} 기본 정보
날짜: {formatDate(data.meetingDate, "kr")} {data.meetingTime && {data.meetingTime}}
{data.location && (
장소: {data.location} {data.address && ({data.address})}
)}
참석 필수: {data.isRequired ? "필수" : "선택"}
{/* 연락처 정보 */} {(data.contactPerson || data.contactPhone || data.contactEmail) && ( 연락처 정보
{[ data.contactPerson && `담당자: ${data.contactPerson}`, data.contactPhone && `전화: ${data.contactPhone}`, data.contactEmail && `이메일: ${data.contactEmail}` ].filter(Boolean).join(' • ')}
)} {/* 안건 및 준비물 */} {(data.agenda || data.materials) && ( 안건 및 준비물 {data.agenda && (
안건:
{data.agenda}
)} {data.materials && (
준비물:
{data.materials}
)}
)} {/* 비고 */} {data.notes && ( 비고
{data.notes}
)} {/* 관련 문서 */} {data.documents.length > 0 && ( 관련 문서 ({data.documents.length}개)
{data.documents.map((doc) => (
))}
)}
) : null}
); } // PR 문서 다이얼로그 interface PrDocumentsDialogProps { open: boolean; onOpenChange: (open: boolean) => void; bidding: BiddingListItem | null; } export function PrDocumentsDialog({ open, onOpenChange, bidding }: PrDocumentsDialogProps) { const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(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 || ""}`; }; return ( PR 문서 {bidding?.title}의 PR 문서 및 아이템 정보입니다. {loading ? (

로딩 중...

) : error ? (

{error}

) : data ? (
{/* PR 문서 목록 */} {data.documents.length > 0 && ( PR 문서 ({data.documents.length}개) 문서명 파일명 버전 크기 등록일 등록자 다운로드 {data.documents.map((doc) => ( {doc.documentName} {doc.description && (
{doc.description}
)}
{doc.version ? ( {doc.version} ) : "-"} {formatFileSize(doc.fileSize)} {new Date(doc.registeredAt).toLocaleDateString('ko-KR')} {doc.registeredBy || "-"}
))}
)} {/* PR 아이템 테이블 */} {data.items.length > 0 && ( PR 아이템 ({data.items.length}개) 아이템 번호 PR 번호 아이템 정보 수량 단가 중량 요청 납기 스펙 문서 {data.items.map((item) => ( {item.itemNumber} {item.prNumber || "-"}
{item.itemInfo && (
{item.itemInfo}
)} {item.materialDescription && (
{item.materialDescription}
)}
{item.quantity ? `${item.quantity.toLocaleString()} ${item.quantityUnit || ""}` : "-"}
{formatCurrency(item.annualUnitPrice, item.currency)}
{formatWeight(item.totalWeight, item.weightUnit)}
{item.requestedDeliveryDate ? (
{new Date(item.requestedDeliveryDate).toLocaleDateString('ko-KR')}
) : "-"}
{item.hasSpecDocument ? "있음" : "없음"} {item.specDocuments.length > 0 && ( ({item.specDocuments.length}개) )}
{item.specDocuments.length > 0 && (
{item.specDocuments.map((doc, index) => (
))}
)}
))}
)} {/* 데이터가 없는 경우 */} {data.documents.length === 0 && data.items.length === 0 && (

PR 문서가 없습니다.

)}
) : null}
); }