diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-14 06:43:13 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-14 06:43:13 +0000 |
| commit | 058b32e0e5ab5bc6fd02fe57b3dde6e934f91040 (patch) | |
| tree | ffe4a25bc3d0f31a41eef399ed633c12a51e420a /lib/bidding/vendor | |
| parent | 675b4e3d8ffcb57a041db285417d81e61284d900 (diff) | |
(최겸) 입찰 긴급여부 추가, 입찰첨부문서 추가
Diffstat (limited to 'lib/bidding/vendor')
| -rw-r--r-- | lib/bidding/vendor/partners-bidding-attachments-dialog.tsx | 246 | ||||
| -rw-r--r-- | lib/bidding/vendor/partners-bidding-list-columns.tsx | 57 | ||||
| -rw-r--r-- | lib/bidding/vendor/partners-bidding-list.tsx | 16 |
3 files changed, 318 insertions, 1 deletions
diff --git a/lib/bidding/vendor/partners-bidding-attachments-dialog.tsx b/lib/bidding/vendor/partners-bidding-attachments-dialog.tsx new file mode 100644 index 00000000..951923ca --- /dev/null +++ b/lib/bidding/vendor/partners-bidding-attachments-dialog.tsx @@ -0,0 +1,246 @@ +'use client' + +import * as React 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 { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table' +import { + FileText, + Download, + Calendar, + User, + Paperclip +} from 'lucide-react' +import { useToast } from '@/hooks/use-toast' +import { useTransition } from 'react' +import { downloadFile } from '@/lib/file-download' +import { getBiddingDocumentsForPartners } from '../detail/service' + +interface UploadedDocument { + id: number + biddingId: number + companyId: number | null + documentType: string + fileName: string + originalFileName: string + fileSize: number | null + filePath: string + title: string | null + description: string | null + uploadedAt: string + uploadedBy: string +} + +interface PartnersBiddingAttachmentsDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + biddingId: number + biddingTitle: string +} + +const documentTypes = [ + { value: 'notice', label: '입찰공고서' }, + { value: 'specification', label: '사양서' }, + { value: 'specification_meeting', label: '사양설명회' }, + { value: 'contract_draft', label: '계약서 초안' }, + { value: 'financial_doc', label: '재무 관련 문서' }, + { value: 'technical_doc', label: '기술 관련 문서' }, + { value: 'certificate', label: '인증서류' }, + { value: 'pr_document', label: 'PR 문서' }, + { value: 'spec_document', label: 'SPEC 문서' }, + { value: 'evaluation_doc', label: '평가 관련 문서' }, + { value: 'bid_attachment', label: '입찰 첨부파일' }, + { value: 'other', label: '기타' } +] + +export function PartnersBiddingAttachmentsDialog({ + open, + onOpenChange, + biddingId, + biddingTitle +}: PartnersBiddingAttachmentsDialogProps) { + const { toast } = useToast() + const [isPending, startTransition] = useTransition() + const [documents, setDocuments] = React.useState<UploadedDocument[]>([]) + const [isLoading, setIsLoading] = React.useState(false) + + // 다이얼로그가 열릴 때 첨부파일 목록 로드 + React.useEffect(() => { + if (open) { + loadDocuments() + } + }, [open, biddingId]) + + const loadDocuments = async () => { + setIsLoading(true) + try { + const docs = await getBiddingDocumentsForPartners(biddingId) + + // Date를 string으로 변환 + const mappedDocs = docs.map(doc => ({ + ...doc, + uploadedAt: doc.uploadedAt?.toString() || '', + uploadedBy: doc.uploadedBy || '' + })) + setDocuments(mappedDocs) + } catch (error) { + console.error('Failed to load documents:', error) + toast({ + title: '오류', + description: '첨부파일 목록을 불러오는데 실패했습니다.', + variant: 'destructive', + }) + } finally { + setIsLoading(false) + } + } + + // 파일 다운로드 + const handleDownload = async (document: UploadedDocument) => { + startTransition(async () => { + try { + if (document.filePath && document.originalFileName) { + await downloadFile(document.filePath, document.originalFileName, { + showToast: true + }) + } else { + throw new Error('파일 정보가 없습니다.') + } + } catch (error) { + toast({ + title: '다운로드 실패', + description: '파일 다운로드에 실패했습니다.', + variant: 'destructive', + }) + } + }) + } + + // 파일 크기 포맷팅 + const formatFileSize = (bytes: number | null) => { + if (!bytes) return '-' + if (bytes === 0) return '0 Bytes' + const k = 1024 + const sizes = ['Bytes', 'KB', 'MB', 'GB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] + } + + const getDocumentTypeLabel = (type: string) => { + return documentTypes.find(dt => dt.value === type)?.label || type + } + + return ( + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto"> + <DialogHeader> + <DialogTitle className="flex items-center gap-2"> + <Paperclip className="w-5 h-5" /> + <span>입찰 첨부파일</span> + <span className="text-sm font-normal text-muted-foreground"> + - {biddingTitle} + </span> + </DialogTitle> + <DialogDescription> + SHI에서 제공한 입찰 관련 첨부파일 목록입니다. + </DialogDescription> + </DialogHeader> + + {isLoading ? ( + <div className="flex items-center justify-center py-12"> + <div className="text-center"> + <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div> + <p className="text-muted-foreground">첨부파일 목록을 불러오는 중...</p> + </div> + </div> + ) : documents.length > 0 ? ( + <div className="space-y-4"> + <div className="flex items-center justify-between"> + <Badge variant="secondary" className="text-sm"> + 총 {documents.length}개 파일 + </Badge> + </div> + + <Table> + <TableHeader> + <TableRow> + <TableHead>문서 타입</TableHead> + <TableHead>파일명</TableHead> + <TableHead>크기</TableHead> + <TableHead>업로드일</TableHead> + <TableHead>작성자</TableHead> + <TableHead className="w-24">작업</TableHead> + </TableRow> + </TableHeader> + <TableBody> + {documents.map((doc) => ( + <TableRow key={doc.id}> + <TableCell> + <Badge variant="outline"> + {getDocumentTypeLabel(doc.documentType)} + </Badge> + </TableCell> + <TableCell> + <div className="flex items-center gap-2"> + <FileText className="w-4 h-4 text-gray-500" /> + <span className="truncate max-w-48" title={doc.originalFileName}> + {doc.originalFileName} + </span> + </div> + </TableCell> + <TableCell className="text-sm text-gray-500"> + {formatFileSize(doc.fileSize)} + </TableCell> + <TableCell className="text-sm text-gray-500"> + <div className="flex items-center gap-1"> + <Calendar className="w-3 h-3" /> + {new Date(doc.uploadedAt).toLocaleDateString('ko-KR')} + </div> + </TableCell> + <TableCell className="text-sm text-gray-500"> + <div className="flex items-center gap-1"> + <User className="w-3 h-3" /> + {doc.uploadedBy} + </div> + </TableCell> + <TableCell> + <Button + variant="outline" + size="sm" + onClick={() => handleDownload(doc)} + disabled={isPending} + title="다운로드" + > + <Download className="w-3 h-3" /> + </Button> + </TableCell> + </TableRow> + ))} + </TableBody> + </Table> + </div> + ) : ( + <div className="text-center py-12 text-gray-500"> + <Paperclip className="w-12 h-12 mx-auto mb-4 opacity-50" /> + <p className="text-lg font-medium mb-2">첨부파일이 없습니다</p> + <p className="text-sm">SHI에서 제공한 첨부파일이 아직 없습니다.</p> + </div> + )} + </DialogContent> + </Dialog> + ) +} diff --git a/lib/bidding/vendor/partners-bidding-list-columns.tsx b/lib/bidding/vendor/partners-bidding-list-columns.tsx index 7058f026..431f7e9a 100644 --- a/lib/bidding/vendor/partners-bidding-list-columns.tsx +++ b/lib/bidding/vendor/partners-bidding-list-columns.tsx @@ -18,7 +18,9 @@ import { MoreHorizontal, Calendar, User, - Calculator + Calculator, + Paperclip, + AlertTriangle } from 'lucide-react' import { formatDate } from '@/lib/utils' import { biddingStatusLabels, contractTypeLabels } from '@/db/schema' @@ -91,6 +93,59 @@ export function getPartnersBiddingListColumns({ setRowAction }: PartnersBiddingL }, }), + // 긴급여부 + columnHelper.accessor('isUrgent', { + header: '긴급여부', + cell: ({ row }) => { + const isUrgent = row.original.isUrgent + + return isUrgent ? ( + <div className="flex items-center gap-1"> + <AlertTriangle className="h-4 w-4 text-red-600" /> + <Badge variant="destructive" className="text-xs"> + 긴급 + </Badge> + </div> + ) : ( + <div className="flex items-center gap-1"> + <CheckCircle className="h-4 w-4 text-green-600" /> + <span className="text-xs text-muted-foreground">일반</span> + </div> + ) + }, + }), + + // 첨부파일 + columnHelper.display({ + id: 'attachments', + header: 'SHI 첨부파일', + cell: ({ row }) => { + const handleViewDocumentsClick = (e: React.MouseEvent) => { + e.stopPropagation() + if (setRowAction) { + setRowAction({ + type: 'view-documents', + row: { original: row.original } + }) + } + } + + return ( + <Button + variant="ghost" + size="sm" + className="p-1 h-8 w-8" + onClick={handleViewDocumentsClick} + title="첨부파일 보기" + > + <Paperclip className="h-4 w-4 text-blue-600" /> + </Button> + ) + }, + size: 80, + enableSorting: false, + }), + // 액션 (드롭다운 메뉴) columnHelper.display({ id: 'actions', diff --git a/lib/bidding/vendor/partners-bidding-list.tsx b/lib/bidding/vendor/partners-bidding-list.tsx index 2fcf1bab..eb38ce71 100644 --- a/lib/bidding/vendor/partners-bidding-list.tsx +++ b/lib/bidding/vendor/partners-bidding-list.tsx @@ -17,6 +17,7 @@ import { getBiddingListForPartners, PartnersBiddingListItem } from '../detail/se import { PartnersBiddingToolbarActions } from './partners-bidding-toolbar-actions' import { PartnersBiddingAttendanceDialog } from './partners-bidding-attendance-dialog' import { PartnersBiddingParticipationDialog } from './partners-bidding-participation-dialog' +import { PartnersBiddingAttachmentsDialog } from './partners-bidding-attachments-dialog' import { setPreQuoteParticipation, getBiddingCompaniesForPartners } from '../pre-quote/service' interface PartnersBiddingListProps { @@ -31,6 +32,8 @@ export function PartnersBiddingList({ companyId }: PartnersBiddingListProps) { const [isParticipationDialogOpen, setIsParticipationDialogOpen] = React.useState(false) const [selectedBiddingForParticipation, setSelectedBiddingForParticipation] = React.useState<PartnersBiddingListItem | null>(null) const [selectedBiddingForPreQuoteParticipation, setSelectedBiddingForPreQuoteParticipation] = React.useState<any | null>(null) + const [isAttachmentsDialogOpen, setIsAttachmentsDialogOpen] = React.useState(false) + const [selectedBiddingForAttachments, setSelectedBiddingForAttachments] = React.useState<PartnersBiddingListItem | null>(null) const router = useRouter() const { toast } = useToast() @@ -124,6 +127,12 @@ export function PartnersBiddingList({ companyId }: PartnersBiddingListProps) { handlePreQuoteParticipationDecision(true) setRowAction(null) // rowAction 초기화 break + case 'view-documents': + // 첨부파일 다이얼로그 열기 + setSelectedBiddingForAttachments(rowAction.row.original) + setIsAttachmentsDialogOpen(true) + setRowAction(null) // rowAction 초기화 + break default: break } @@ -255,6 +264,13 @@ export function PartnersBiddingList({ companyId }: PartnersBiddingListProps) { }} /> */} + <PartnersBiddingAttachmentsDialog + open={isAttachmentsDialogOpen} + onOpenChange={setIsAttachmentsDialogOpen} + biddingId={selectedBiddingForAttachments?.biddingId || 0} + biddingTitle={selectedBiddingForAttachments?.title || ''} + /> + </> ) } |
