"use client" import * as React from "react" import { useState, useEffect } from "react" import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@/components/ui/dialog" import { Button } from "@/components/ui/button" import { Badge } from "@/components/ui/badge" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" 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, MapPinIcon, FileTextIcon, ExternalLinkIcon, FileXIcon, UploadIcon } from "lucide-react" import { BiddingListItem } from "@/db/schema" import { downloadFile, formatFileSize, getFileInfo } from "@/lib/file-download" 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; 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; }>; } // PR 관련 타입과 컴포넌트는 bidding-pr-documents-dialog.tsx로 이동됨 // 파일 다운로드 훅 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 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 문서 다이얼로그는 bidding-pr-documents-dialog.tsx로 이동됨 // import { PrDocumentsDialog } from './bidding-pr-documents-dialog'로 사용하세요 // 폐찰하기 다이얼로그 interface BidClosureDialogProps { open: boolean; onOpenChange: (open: boolean) => void; bidding: BiddingListItem | null; userId: string; } export function BidClosureDialog({ open, onOpenChange, bidding, userId }: BidClosureDialogProps) { const [description, setDescription] = useState('') const [files, setFiles] = useState([]) 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) => { if (e.target.files) { setFiles(Array.from(e.target.files)) } } if (!bidding) return null return ( 폐찰하기 {bidding.title} ({bidding.biddingNumber})를 폐찰합니다.