summaryrefslogtreecommitdiff
path: root/lib/bidding/vendor
diff options
context:
space:
mode:
Diffstat (limited to 'lib/bidding/vendor')
-rw-r--r--lib/bidding/vendor/components/pr-items-pricing-table.tsx143
-rw-r--r--lib/bidding/vendor/export-partners-biddings-to-excel.ts275
-rw-r--r--lib/bidding/vendor/partners-bidding-attendance-dialog.tsx1
-rw-r--r--lib/bidding/vendor/partners-bidding-detail.tsx18
-rw-r--r--lib/bidding/vendor/partners-bidding-list-columns.tsx56
-rw-r--r--lib/bidding/vendor/partners-bidding-list.tsx1
-rw-r--r--lib/bidding/vendor/partners-bidding-toolbar-actions.tsx34
7 files changed, 468 insertions, 60 deletions
diff --git a/lib/bidding/vendor/components/pr-items-pricing-table.tsx b/lib/bidding/vendor/components/pr-items-pricing-table.tsx
index 7dd8384e..6910e360 100644
--- a/lib/bidding/vendor/components/pr-items-pricing-table.tsx
+++ b/lib/bidding/vendor/components/pr-items-pricing-table.tsx
@@ -4,7 +4,17 @@ import * as React from 'react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
-
+import { Button } from '@/components/ui/button'
+import { Calendar } from '@/components/ui/calendar'
+import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog'
import { Badge } from '@/components/ui/badge'
import {
Table,
@@ -16,10 +26,12 @@ import {
} from '@/components/ui/table'
import {
Package,
-
Download,
- Calculator
+ Calculator,
+ CalendarIcon
} from 'lucide-react'
+import { format } from 'date-fns'
+import { cn } from '@/lib/utils'
import { formatDate } from '@/lib/utils'
import { downloadFile, formatFileSize, getFileInfo } from '@/lib/file-download'
import { getSpecDocumentsForPrItem } from '../../pre-quote/service'
@@ -186,6 +198,8 @@ export function PrItemsPricingTable({
}: PrItemsPricingTableProps) {
const [quotations, setQuotations] = React.useState<PrItemQuotation[]>([])
const [specDocuments, setSpecDocuments] = React.useState<Record<number, SpecDocument[]>>({})
+ const [showBulkDateDialog, setShowBulkDateDialog] = React.useState(false)
+ const [bulkDeliveryDate, setBulkDeliveryDate] = React.useState<Date | undefined>(undefined)
// 초기 견적 데이터 설정 및 SPEC 문서 로드
React.useEffect(() => {
@@ -279,6 +293,21 @@ export function PrItemsPricingTable({
onTotalAmountChange(totalAmount)
}
+ // 일괄 납기일 적용
+ const applyBulkDeliveryDate = () => {
+ if (bulkDeliveryDate && quotations.length > 0) {
+ const formattedDate = format(bulkDeliveryDate, 'yyyy-MM-dd')
+ const updatedQuotations = quotations.map(q => ({
+ ...q,
+ proposedDeliveryDate: formattedDate
+ }))
+
+ setQuotations(updatedQuotations)
+ onQuotationsChange(updatedQuotations)
+ setShowBulkDateDialog(false)
+ setBulkDeliveryDate(undefined)
+ }
+ }
// 통화 포맷팅
const formatCurrency = (amount: number) => {
@@ -292,12 +321,26 @@ export function PrItemsPricingTable({
const totalAmount = quotations.reduce((sum, q) => sum + q.bidAmount, 0)
return (
+ <>
<Card>
<CardHeader>
- <CardTitle className="flex items-center gap-2">
- <Package className="w-5 h-5" />
- 품목별 입찰 작성
- </CardTitle>
+ <div className="flex items-center justify-between">
+ <CardTitle className="flex items-center gap-2">
+ <Package className="w-5 h-5" />
+ 품목별 입찰 작성
+ </CardTitle>
+ {!readOnly && (
+ <Button
+ type="button"
+ variant="outline"
+ size="sm"
+ onClick={() => setShowBulkDateDialog(true)}
+ >
+ <CalendarIcon className="h-4 w-4 mr-1" />
+ 전체 납품예정일 설정
+ </Button>
+ )}
+ </div>
</CardHeader>
<CardContent>
<div className="space-y-4">
@@ -382,18 +425,14 @@ export function PrItemsPricingTable({
</span>
) : (
<Input
- type="number"
- inputMode="decimal"
- min={0}
- pattern="^(0|[1-9][0-9]*)(\.[0-9]+)?$"
- value={quotation.bidUnitPrice === 0 ? '' : quotation.bidUnitPrice}
+ type="text"
+ inputMode="numeric"
+ value={quotation.bidUnitPrice === 0 ? '' : quotation.bidUnitPrice.toLocaleString()}
onChange={(e) => {
- let value = e.target.value
- if (/^0[0-9]+/.test(value)) {
- value = value.replace(/^0+/, '')
- if (value === '') value = '0'
- }
- const numericValue = parseFloat(value)
+ // 콤마 제거 및 숫자만 허용
+ const value = e.target.value.replace(/,/g, '').replace(/[^0-9]/g, '')
+ const numericValue = Number(value)
+
updateQuotation(
item.id,
'bidUnitPrice',
@@ -471,5 +510,73 @@ export function PrItemsPricingTable({
</div>
</CardContent>
</Card>
+
+ {/* 일괄 납품예정일 설정 다이얼로그 */}
+ <Dialog open={showBulkDateDialog} onOpenChange={setShowBulkDateDialog}>
+ <DialogContent className="sm:max-w-md">
+ <DialogHeader>
+ <DialogTitle>전체 납품예정일 설정</DialogTitle>
+ <DialogDescription>
+ 모든 PR 아이템에 동일한 납품예정일을 적용합니다.
+ </DialogDescription>
+ </DialogHeader>
+
+ <div className="space-y-4">
+ <div className="space-y-2">
+ <Label>납품예정일 선택</Label>
+ <Popover>
+ <PopoverTrigger asChild>
+ <Button
+ variant="outline"
+ className={cn(
+ "w-full justify-start text-left font-normal",
+ !bulkDeliveryDate && "text-muted-foreground"
+ )}
+ >
+ <CalendarIcon className="mr-2 h-4 w-4" />
+ {bulkDeliveryDate ? format(bulkDeliveryDate, "yyyy-MM-dd") : "날짜 선택"}
+ </Button>
+ </PopoverTrigger>
+ <PopoverContent className="w-auto p-0" align="start">
+ <Calendar
+ mode="single"
+ selected={bulkDeliveryDate}
+ onSelect={setBulkDeliveryDate}
+ initialFocus
+ />
+ </PopoverContent>
+ </Popover>
+ </div>
+
+ <div className="bg-muted/50 rounded-lg p-3">
+ <p className="text-sm text-muted-foreground">
+ 선택된 날짜가 <strong>{prItems.length}개</strong>의 모든 PR 아이템에 적용됩니다.
+ 기존에 설정된 납품예정일은 모두 교체됩니다.
+ </p>
+ </div>
+ </div>
+
+ <DialogFooter>
+ <Button
+ type="button"
+ variant="outline"
+ onClick={() => {
+ setShowBulkDateDialog(false)
+ setBulkDeliveryDate(undefined)
+ }}
+ >
+ 취소
+ </Button>
+ <Button
+ type="button"
+ onClick={applyBulkDeliveryDate}
+ disabled={!bulkDeliveryDate}
+ >
+ 전체 적용
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ </>
)
}
diff --git a/lib/bidding/vendor/export-partners-biddings-to-excel.ts b/lib/bidding/vendor/export-partners-biddings-to-excel.ts
new file mode 100644
index 00000000..e1d985fe
--- /dev/null
+++ b/lib/bidding/vendor/export-partners-biddings-to-excel.ts
@@ -0,0 +1,275 @@
+import { type Table } from "@tanstack/react-table"
+import ExcelJS from "exceljs"
+import { PartnersBiddingListItem } from '../detail/service'
+import {
+ biddingStatusLabels,
+ contractTypeLabels,
+} from "@/db/schema"
+import { formatDate } from "@/lib/utils"
+
+/**
+ * Partners 입찰 목록을 Excel로 내보내기
+ * - 계약구분, 진행상태는 라벨(명칭)로 변환
+ * - 입찰기간은 submissionStartDate, submissionEndDate 기준
+ * - 날짜는 적절한 형식으로 변환
+ */
+export async function exportPartnersBiddingsToExcel(
+ table: Table<PartnersBiddingListItem>,
+ {
+ filename = "협력업체입찰목록",
+ onlySelected = false,
+ }: {
+ filename?: string
+ onlySelected?: boolean
+ } = {}
+): Promise<void> {
+ // 테이블에서 실제 사용 중인 leaf columns 가져오기
+ const allColumns = table.getAllLeafColumns()
+
+ // select, actions, attachments 컬럼 제외
+ const columns = allColumns.filter(
+ (col) => !["select", "actions", "attachments"].includes(col.id)
+ )
+
+ // 헤더 매핑 (컬럼 id -> Excel 헤더명)
+ const headerMap: Record<string, string> = {
+ biddingNumber: "입찰 No.",
+ status: "입찰상태",
+ isUrgent: "긴급여부",
+ title: "입찰명",
+ isAttendingMeeting: "사양설명회",
+ isBiddingParticipated: "입찰 참여의사",
+ biddingSubmissionStatus: "입찰 제출여부",
+ contractType: "계약구분",
+ submissionStartDate: "입찰기간",
+ contractStartDate: "계약기간",
+ bidPicName: "입찰담당자",
+ supplyPicName: "조달담당자",
+ updatedAt: "최종수정일",
+ }
+
+ // 헤더 행 생성
+ const headerRow = columns.map((col) => {
+ return headerMap[col.id] || col.id
+ })
+
+ // 데이터 행 생성
+ const rowModel = onlySelected
+ ? table.getFilteredSelectedRowModel()
+ : table.getRowModel()
+
+ const dataRows = rowModel.rows.map((row) => {
+ const original = row.original
+ return columns.map((col) => {
+ const colId = col.id
+ let value: any
+
+ // 특별 처리 필요한 컬럼들
+ switch (colId) {
+ case "contractType":
+ // 계약구분: 라벨로 변환
+ value = contractTypeLabels[original.contractType as keyof typeof contractTypeLabels] || original.contractType
+ break
+
+ case "status":
+ // 입찰상태: 라벨로 변환
+ value = biddingStatusLabels[original.status as keyof typeof biddingStatusLabels] || original.status
+ break
+
+ case "isUrgent":
+ // 긴급여부: Yes/No
+ value = original.isUrgent ? "긴급" : "일반"
+ break
+
+ case "isAttendingMeeting":
+ // 사양설명회: 참석/불참/미결정
+ if (original.isAttendingMeeting === null) {
+ value = "해당없음"
+ } else {
+ value = original.isAttendingMeeting ? "참석" : "불참"
+ }
+ break
+
+ case "isBiddingParticipated":
+ // 입찰 참여의사: 참여/불참/미결정
+ if (original.isBiddingParticipated === null) {
+ value = "미결정"
+ } else {
+ value = original.isBiddingParticipated ? "참여" : "불참"
+ }
+ break
+
+ case "biddingSubmissionStatus":
+ // 입찰 제출여부: 최종제출/제출/미제출
+ const finalQuoteAmount = original.finalQuoteAmount
+ const isFinalSubmission = original.isFinalSubmission
+
+ if (!finalQuoteAmount) {
+ value = "미제출"
+ } else if (isFinalSubmission) {
+ value = "최종제출"
+ } else {
+ value = "제출"
+ }
+ break
+
+ case "submissionStartDate":
+ // 입찰기간: submissionStartDate, submissionEndDate 기준
+ const startDate = original.submissionStartDate
+ const endDate = original.submissionEndDate
+
+ if (!startDate || !endDate) {
+ value = "-"
+ } else {
+ const startObj = new Date(startDate)
+ const endObj = new Date(endDate)
+
+ // 입력값 기반: 저장된 UTC 값을 그대로 표시 (타임존 가감 없음)
+ const formatValue = (d: Date) => d.toISOString().slice(0, 16).replace('T', ' ')
+
+ value = `${formatValue(startObj)} ~ ${formatValue(endObj)}`
+ }
+ break
+
+ // case "preQuoteDeadline":
+ // // 사전견적 마감일: 날짜 형식
+ // if (!original.preQuoteDeadline) {
+ // value = "-"
+ // } else {
+ // const deadline = new Date(original.preQuoteDeadline)
+ // value = deadline.toISOString().slice(0, 16).replace('T', ' ')
+ // }
+ // break
+
+ case "contractStartDate":
+ // 계약기간: contractStartDate, contractEndDate 기준
+ const contractStart = original.contractStartDate
+ const contractEnd = original.contractEndDate
+
+ if (!contractStart || !contractEnd) {
+ value = "-"
+ } else {
+ const startObj = new Date(contractStart)
+ const endObj = new Date(contractEnd)
+ value = `${formatDate(startObj, "KR")} ~ ${formatDate(endObj, "KR")}`
+ }
+ break
+
+ case "bidPicName":
+ // 입찰담당자: bidPicName
+ value = original.bidPicName || "-"
+ break
+
+ case "supplyPicName":
+ // 조달담당자: supplyPicName
+ value = original.supplyPicName || "-"
+ break
+
+ case "updatedAt":
+ // 최종수정일: 날짜 시간 형식
+ if (original.updatedAt) {
+ const updated = new Date(original.updatedAt)
+ value = updated.toISOString().slice(0, 16).replace('T', ' ')
+ } else {
+ value = "-"
+ }
+ break
+
+ case "biddingNumber":
+ // 입찰번호: 원입찰번호 포함
+ const biddingNumber = original.biddingNumber
+ const originalBiddingNumber = original.originalBiddingNumber
+ if (originalBiddingNumber) {
+ value = `${biddingNumber} (원: ${originalBiddingNumber})`
+ } else {
+ value = biddingNumber
+ }
+ break
+
+ default:
+ // 기본값: row.getValue 사용
+ value = row.getValue(colId)
+
+ // null/undefined 처리
+ if (value == null) {
+ value = ""
+ }
+
+ // 객체인 경우 JSON 문자열로 변환
+ if (typeof value === "object") {
+ value = JSON.stringify(value)
+ }
+ break
+ }
+
+ return value
+ })
+ })
+
+ // 최종 sheetData
+ const sheetData = [headerRow, ...dataRows]
+
+ // ExcelJS로 파일 생성 및 다운로드
+ await createAndDownloadExcel(sheetData, columns.length, filename)
+}
+
+/**
+ * Excel 파일 생성 및 다운로드
+ */
+async function createAndDownloadExcel(
+ sheetData: any[][],
+ columnCount: number,
+ filename: string
+): Promise<void> {
+ // ExcelJS 워크북/시트 생성
+ const workbook = new ExcelJS.Workbook()
+ const worksheet = workbook.addWorksheet("Sheet1")
+
+ // 칼럼별 최대 길이 추적
+ const maxColumnLengths = Array(columnCount).fill(0)
+ sheetData.forEach((row) => {
+ row.forEach((cellValue, colIdx) => {
+ const cellText = cellValue?.toString() ?? ""
+ if (cellText.length > maxColumnLengths[colIdx]) {
+ maxColumnLengths[colIdx] = cellText.length
+ }
+ })
+ })
+
+ // 시트에 데이터 추가 + 헤더 스타일
+ sheetData.forEach((arr, idx) => {
+ const row = worksheet.addRow(arr)
+
+ // 헤더 스타일 적용 (첫 번째 행)
+ if (idx === 0) {
+ row.font = { bold: true }
+ row.alignment = { horizontal: "center" }
+ row.eachCell((cell) => {
+ cell.fill = {
+ type: "pattern",
+ pattern: "solid",
+ fgColor: { argb: "FFCCCCCC" },
+ }
+ })
+ }
+ })
+
+ // 칼럼 너비 자동 조정
+ maxColumnLengths.forEach((len, idx) => {
+ // 최소 너비 10, +2 여백
+ worksheet.getColumn(idx + 1).width = Math.max(len + 2, 10)
+ })
+
+ // 최종 파일 다운로드
+ const buffer = await workbook.xlsx.writeBuffer()
+ const blob = new Blob([buffer], {
+ type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ })
+ const url = URL.createObjectURL(blob)
+ const link = document.createElement("a")
+ link.href = url
+ link.download = `${filename}.xlsx`
+ link.click()
+ URL.revokeObjectURL(url)
+}
+
diff --git a/lib/bidding/vendor/partners-bidding-attendance-dialog.tsx b/lib/bidding/vendor/partners-bidding-attendance-dialog.tsx
index d0ef97f1..8d6cb82d 100644
--- a/lib/bidding/vendor/partners-bidding-attendance-dialog.tsx
+++ b/lib/bidding/vendor/partners-bidding-attendance-dialog.tsx
@@ -37,7 +37,6 @@ interface PartnersSpecificationMeetingDialogProps {
title: string
preQuoteDate: string | null
biddingRegistrationDate: string | null
- evaluationDate: string | null
hasSpecificationMeeting?: boolean // 사양설명회 여부 추가
} | null
biddingCompanyId: number
diff --git a/lib/bidding/vendor/partners-bidding-detail.tsx b/lib/bidding/vendor/partners-bidding-detail.tsx
index bf76de62..bf33cef5 100644
--- a/lib/bidding/vendor/partners-bidding-detail.tsx
+++ b/lib/bidding/vendor/partners-bidding-detail.tsx
@@ -75,7 +75,6 @@ interface BiddingDetail {
biddingRegistrationDate: Date | string | null
submissionStartDate: Date | string | null
submissionEndDate: Date | string | null
- evaluationDate: Date | string | null
currency: string
budget: number | null
targetPrice: number | null
@@ -869,7 +868,8 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD
const timeLeft = deadline.getTime() - now.getTime()
const daysLeft = Math.floor(timeLeft / (1000 * 60 * 60 * 24))
const hoursLeft = Math.floor((timeLeft % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))
- const kstDeadline = new Date(deadline.getTime() + 9 * 60 * 60 * 1000).toISOString().slice(0, 16).replace('T', ' ')
+ // 입력값 기반: 저장된 UTC 값을 그대로 표시
+ const displayDeadline = deadline.toISOString().slice(0, 16).replace('T', ' ')
return (
<div className={`p-3 rounded-lg border-2 ${
@@ -884,7 +884,7 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD
<Calendar className="w-5 h-5" />
<span className="font-medium">제출 마감일:</span>
<span className="text-lg font-semibold">
- {kstDeadline}
+ {displayDeadline}
</span>
</div>
{isExpired ? (
@@ -921,17 +921,13 @@ export function PartnersBiddingDetail({ biddingId, companyId }: PartnersBiddingD
<span className="font-medium">입찰서 제출기간:</span> {(() => {
const start = new Date(biddingDetail.submissionStartDate!)
const end = new Date(biddingDetail.submissionEndDate!)
- const kstStart = new Date(start.getTime() + 9 * 60 * 60 * 1000).toISOString().slice(0, 16).replace('T', ' ')
- const kstEnd = new Date(end.getTime() + 9 * 60 * 60 * 1000).toISOString().slice(0, 16).replace('T', ' ')
- return `${kstStart} ~ ${kstEnd}`
+ const displayStart = start.toISOString().slice(0, 16).replace('T', ' ')
+ const displayEnd = end.toISOString().slice(0, 16).replace('T', ' ')
+ return `${displayStart} ~ ${displayEnd}`
})()}
</div>
)}
- {biddingDetail.evaluationDate && (
- <div>
- <span className="font-medium">평가일:</span> {format(new Date(biddingDetail.evaluationDate), "yyyy-MM-dd HH:mm")}
- </div>
- )}
+
</div>
</div>
</CardContent>
diff --git a/lib/bidding/vendor/partners-bidding-list-columns.tsx b/lib/bidding/vendor/partners-bidding-list-columns.tsx
index a122e87b..09c3caad 100644
--- a/lib/bidding/vendor/partners-bidding-list-columns.tsx
+++ b/lib/bidding/vendor/partners-bidding-list-columns.tsx
@@ -285,7 +285,7 @@ export function getPartnersBiddingListColumns({ setRowAction }: PartnersBiddingL
cell: ({ row }) => {
const isAttending = row.original.isAttendingMeeting
if (isAttending === null) {
- return <div className="text-muted-foreground text-center">-</div>
+ return <div className="text-muted-foreground text-center">해당없음</div>
}
return isAttending ? (
<CheckCircle className="h-5 w-5 text-green-600 mx-auto" />
@@ -352,45 +352,45 @@ export function getPartnersBiddingListColumns({ setRowAction }: PartnersBiddingL
const startObj = new Date(startDate)
const endObj = new Date(endDate)
- // UI 표시용 KST 변환
- const formatKst = (d: Date) => new Date(d.getTime() + 9 * 60 * 60 * 1000).toISOString().slice(0, 16).replace('T', ' ')
+ // 입력값 기반: 저장된 UTC 값 그대로 표시 (타임존 가감 없음)
+ const formatValue = (d: Date) => d.toISOString().slice(0, 16).replace('T', ' ')
return (
<div className="text-sm">
- <div>{formatKst(startObj)}</div>
+ <div>{formatValue(startObj)}</div>
<div className="text-muted-foreground">~</div>
- <div>{formatKst(endObj)}</div>
+ <div>{formatValue(endObj)}</div>
</div>
)
},
}),
// 사전견적 마감일
- columnHelper.accessor('preQuoteDeadline', {
- header: '사전견적 마감일',
- cell: ({ row }) => {
- const deadline = row.original.preQuoteDeadline
- if (!deadline) {
- return <div className="text-muted-foreground">-</div>
- }
+ // columnHelper.accessor('preQuoteDeadline', {
+ // header: '사전견적 마감일',
+ // cell: ({ row }) => {
+ // const deadline = row.original.preQuoteDeadline
+ // if (!deadline) {
+ // return <div className="text-muted-foreground">-</div>
+ // }
- const now = new Date()
- const deadlineDate = new Date(deadline)
- const isExpired = deadlineDate < now
+ // const now = new Date()
+ // const deadlineDate = new Date(deadline)
+ // const isExpired = deadlineDate < now
- return (
- <div className={`text-sm flex items-center gap-1 ${isExpired ? 'text-red-600' : ''}`}>
- <Calendar className="w-4 h-4" />
- <span>{format(new Date(deadline), "yyyy-MM-dd HH:mm")}</span>
- {isExpired && (
- <Badge variant="destructive" className="text-xs">
- 마감
- </Badge>
- )}
- </div>
- )
- },
- }),
+ // return (
+ // <div className={`text-sm flex items-center gap-1 ${isExpired ? 'text-red-600' : ''}`}>
+ // <Calendar className="w-4 h-4" />
+ // <span>{format(new Date(deadline), "yyyy-MM-dd HH:mm")}</span>
+ // {isExpired && (
+ // <Badge variant="destructive" className="text-xs">
+ // 마감
+ // </Badge>
+ // )}
+ // </div>
+ // )
+ // },
+ // }),
// 계약기간
columnHelper.accessor('contractStartDate', {
diff --git a/lib/bidding/vendor/partners-bidding-list.tsx b/lib/bidding/vendor/partners-bidding-list.tsx
index 0f68ed68..f1cb0bdc 100644
--- a/lib/bidding/vendor/partners-bidding-list.tsx
+++ b/lib/bidding/vendor/partners-bidding-list.tsx
@@ -181,7 +181,6 @@ export function PartnersBiddingList({ promises }: PartnersBiddingListProps) {
title: rowAction.row.original.title,
preQuoteDate: null,
biddingRegistrationDate: rowAction.row.original.submissionStartDate?.toISOString() || null,
- evaluationDate: null,
hasSpecificationMeeting: rowAction.row.original.hasSpecificationMeeting || false,
} : null}
biddingCompanyId={rowAction?.row.original?.biddingCompanyId || 0}
diff --git a/lib/bidding/vendor/partners-bidding-toolbar-actions.tsx b/lib/bidding/vendor/partners-bidding-toolbar-actions.tsx
index 87b1367e..9a2f026c 100644
--- a/lib/bidding/vendor/partners-bidding-toolbar-actions.tsx
+++ b/lib/bidding/vendor/partners-bidding-toolbar-actions.tsx
@@ -2,10 +2,12 @@
import * as React from "react"
import { type Table } from "@tanstack/react-table"
-import { Users} from "lucide-react"
+import { Users, FileSpreadsheet } from "lucide-react"
+import { toast } from "sonner"
import { Button } from "@/components/ui/button"
import { PartnersBiddingListItem } from '../detail/service'
+import { exportPartnersBiddingsToExcel } from './export-partners-biddings-to-excel'
interface PartnersBiddingToolbarActionsProps {
table: Table<PartnersBiddingListItem>
@@ -20,6 +22,8 @@ export function PartnersBiddingToolbarActions({
const selectedRows = table.getFilteredSelectedRowModel().rows
const selectedBidding = selectedRows.length === 1 ? selectedRows[0].original : null
+ const [isExporting, setIsExporting] = React.useState(false)
+
const handleSpecificationMeetingClick = () => {
if (selectedBidding && setRowAction) {
setRowAction({
@@ -29,8 +33,36 @@ export function PartnersBiddingToolbarActions({
}
}
+ // Excel 내보내기 핸들러
+ const handleExport = React.useCallback(async () => {
+ try {
+ setIsExporting(true)
+ await exportPartnersBiddingsToExcel(table, {
+ filename: "협력업체입찰목록",
+ onlySelected: false,
+ })
+ toast.success("Excel 파일이 다운로드되었습니다.")
+ } catch (error) {
+ console.error("Excel export error:", error)
+ toast.error("Excel 내보내기 중 오류가 발생했습니다.")
+ } finally {
+ setIsExporting(false)
+ }
+ }, [table])
+
return (
<div className="flex items-center gap-2">
+ {/* Excel 내보내기 버튼 */}
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={handleExport}
+ disabled={isExporting}
+ className="gap-2"
+ >
+ <FileSpreadsheet className="size-4" aria-hidden="true" />
+ <span className="hidden sm:inline">{isExporting ? "내보내는 중..." : "Excel 내보내기"}</span>
+ </Button>
<Button
variant="outline"
size="sm"