From b6c9ac31358206ad291583d4b045cc5d83ca3987 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Thu, 4 Sep 2025 10:51:24 +0000 Subject: (최겸) 구매 입찰내 입찰 조건, 사전견적 기능, 품목별 견적 작성 개발 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vendor/components/pr-items-pricing-table.tsx | 195 +++++++++++++++------ 1 file changed, 138 insertions(+), 57 deletions(-) (limited to 'lib/bidding/vendor/components/pr-items-pricing-table.tsx') diff --git a/lib/bidding/vendor/components/pr-items-pricing-table.tsx b/lib/bidding/vendor/components/pr-items-pricing-table.tsx index 320ed6eb..01885f7a 100644 --- a/lib/bidding/vendor/components/pr-items-pricing-table.tsx +++ b/lib/bidding/vendor/components/pr-items-pricing-table.tsx @@ -22,8 +22,14 @@ import { Calculator } from 'lucide-react' import { formatDate } from '@/lib/utils' -import { downloadFile } from '@/lib/file-download' +import { downloadFile, formatFileSize, getFileInfo } from '@/lib/file-download' import { getSpecDocumentsForPrItem } from '../../pre-quote/service' +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@/components/ui/tooltip' interface PrItem { id: number @@ -57,6 +63,93 @@ interface SpecDocument { uploadedAt: string } +// 파일 다운로드 훅 +const useFileDownload = () => { + const [downloadingFiles, setDownloadingFiles] = React.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 | null + 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 PrItemsPricingTableProps { prItems: PrItem[] initialQuotations?: PrItemQuotation[] @@ -76,9 +169,8 @@ export function PrItemsPricingTable({ }: PrItemsPricingTableProps) { const [quotations, setQuotations] = React.useState([]) const [specDocuments, setSpecDocuments] = React.useState>({}) - const [loadingSpecs, setLoadingSpecs] = React.useState>({}) - // 초기 견적 데이터 설정 + // 초기 견적 데이터 설정 및 SPEC 문서 로드 React.useEffect(() => { const initQuotations = prItems.map(item => { const existing = initialQuotations.find(q => q.prItemId === item.id) @@ -94,27 +186,34 @@ export function PrItemsPricingTable({ } }) setQuotations(initQuotations) - }, [prItems, initialQuotations]) - // SPEC 문서 로드 - const loadSpecDocuments = async (prItemId: number) => { - if (loadingSpecs[prItemId]) return - - setLoadingSpecs(prev => ({ ...prev, [prItemId]: true })) - try { - const docs = await getSpecDocumentsForPrItem(prItemId) - // Date를 string으로 변환 - const mappedDocs = docs.map(doc => ({ - ...doc, - uploadedAt: doc.uploadedAt.toString() - })) - setSpecDocuments(prev => ({ ...prev, [prItemId]: mappedDocs })) - } catch (error) { - console.error('Failed to load spec documents:', error) - } finally { - setLoadingSpecs(prev => ({ ...prev, [prItemId]: false })) + // SPEC 문서가 있는 모든 PR 아이템의 문서를 미리 로드 + const loadAllSpecDocuments = async () => { + const itemsWithSpecs = prItems.filter(item => item.hasSpecDocument) + console.log('Loading spec documents for items:', itemsWithSpecs.map(item => ({ id: item.id, itemNumber: item.itemNumber }))) + + for (const item of itemsWithSpecs) { + try { + console.log('Loading spec documents for prItemId:', item.id) + const docs = await getSpecDocumentsForPrItem(item.id) + console.log('Loaded spec documents for item', item.id, ':', docs) + + // Date를 string으로 변환 + const mappedDocs = docs.map(doc => ({ + ...doc, + uploadedAt: doc.uploadedAt.toString() + })) + + setSpecDocuments(prev => ({ ...prev, [item.id]: mappedDocs })) + } catch (error) { + console.error('Failed to load spec documents for item', item.id, ':', error) + } + } } - } + + loadAllSpecDocuments() + }, [prItems, initialQuotations]) + // 견적 데이터 업데이트 const updateQuotation = (prItemId: number, field: keyof PrItemQuotation, value: any) => { @@ -142,16 +241,6 @@ export function PrItemsPricingTable({ onTotalAmountChange(totalAmount) } - // 파일 다운로드 - const handleDownloadSpec = async (document: SpecDocument) => { - try { - await downloadFile(document.filePath, document.originalFileName, { - showToast: true - }) - } catch (error) { - console.error('Failed to download spec document:', error) - } - } // 통화 포맷팅 const formatCurrency = (amount: number) => { @@ -187,7 +276,7 @@ export function PrItemsPricingTable({ 견적단가 견적금액 납품예정일 - 기술사양 + {/* 기술사양 */} SPEC @@ -262,7 +351,7 @@ export function PrItemsPricingTable({ /> )} - + {/* {readOnly ? (
{quotation.technicalSpecification || '-'} @@ -280,37 +369,29 @@ export function PrItemsPricingTable({ rows={2} /> )} - + */} {item.hasSpecDocument ? (
- {!specDocuments[item.id] ? ( - - ) : specDocuments[item.id].length > 0 ? ( + {specDocuments[item.id] && specDocuments[item.id].length > 0 ? (
{specDocuments[item.id].map((doc) => ( - +
+ +
))}
) : ( - 문서 없음 +
+ 문서 없음 + 로딩 중... +
)}
) : ( -- cgit v1.2.3