summaryrefslogtreecommitdiff
path: root/components/bidding/manage/bidding-items-editor.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'components/bidding/manage/bidding-items-editor.tsx')
-rw-r--r--components/bidding/manage/bidding-items-editor.tsx181
1 files changed, 178 insertions, 3 deletions
diff --git a/components/bidding/manage/bidding-items-editor.tsx b/components/bidding/manage/bidding-items-editor.tsx
index 90e512d2..452cdc3c 100644
--- a/components/bidding/manage/bidding-items-editor.tsx
+++ b/components/bidding/manage/bidding-items-editor.tsx
@@ -1,7 +1,7 @@
'use client'
import * as React from 'react'
-import { Package, Plus, Trash2, Save, RefreshCw, FileText } from 'lucide-react'
+import { Package, Plus, Trash2, Save, RefreshCw, FileText, FileSpreadsheet, Upload } from 'lucide-react'
import { getPRItemsForBidding } from '@/lib/bidding/detail/service'
import { updatePrItem } from '@/lib/bidding/detail/service'
import { toast } from 'sonner'
@@ -26,7 +26,7 @@ import { CostCenterSingleSelector } from '@/components/common/selectors/cost-cen
import { GlAccountSingleSelector } from '@/components/common/selectors/gl-account/gl-account-single-selector'
// PR 아이템 정보 타입 (create-bidding-dialog와 동일)
-interface PRItemInfo {
+export interface PRItemInfo {
id: number // 실제 DB ID
prNumber?: string | null
projectId?: number | null
@@ -84,6 +84,16 @@ import { CreatePreQuoteRfqDialog } from './create-pre-quote-rfq-dialog'
import { ProcurementItemSelectorDialogSingle } from '@/components/common/selectors/procurement-item/procurement-item-selector-dialog-single'
import { Textarea } from '@/components/ui/textarea'
import { Label } from '@/components/ui/label'
+import { exportBiddingItemsToExcel } from '@/lib/bidding/manage/export-bidding-items-to-excel'
+import { importBiddingItemsFromExcel } from '@/lib/bidding/manage/import-bidding-items-from-excel'
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog'
export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItemsEditorProps) {
const { data: session } = useSession()
@@ -114,6 +124,11 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems
isPriceAdjustmentApplicable?: boolean | null
sparePartOptions?: string | null
} | null>(null)
+ const [importDialogOpen, setImportDialogOpen] = React.useState(false)
+ const [importFile, setImportFile] = React.useState<File | null>(null)
+ const [importErrors, setImportErrors] = React.useState<string[]>([])
+ const [isImporting, setIsImporting] = React.useState(false)
+ const [isExporting, setIsExporting] = React.useState(false)
// 초기 데이터 로딩 - 기존 품목이 있으면 자동으로 로드
React.useEffect(() => {
@@ -492,7 +507,7 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems
materialGroupInfo: null,
materialNumber: null,
materialInfo: null,
- priceUnit: 1,
+ priceUnit: '1',
purchaseUnit: 'EA',
materialWeight: null,
wbsCode: null,
@@ -644,6 +659,76 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems
const totals = calculateTotals()
+ // Excel 내보내기 핸들러
+ const handleExport = React.useCallback(async () => {
+ if (items.length === 0) {
+ toast.error('내보낼 품목이 없습니다.')
+ return
+ }
+
+ try {
+ setIsExporting(true)
+ await exportBiddingItemsToExcel(items, {
+ filename: `입찰품목목록_${biddingId}`,
+ })
+ toast.success('Excel 파일이 다운로드되었습니다.')
+ } catch (error) {
+ console.error('Excel export error:', error)
+ toast.error('Excel 내보내기 중 오류가 발생했습니다.')
+ } finally {
+ setIsExporting(false)
+ }
+ }, [items, biddingId])
+
+ // Excel 가져오기 핸들러
+ const handleImportFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
+ const file = event.target.files?.[0]
+ if (file) {
+ if (!file.name.endsWith('.xlsx') && !file.name.endsWith('.xls')) {
+ toast.error('Excel 파일(.xlsx, .xls)만 업로드 가능합니다.')
+ return
+ }
+ setImportFile(file)
+ setImportErrors([])
+ }
+ }
+
+ const handleImport = async () => {
+ if (!importFile) return
+
+ setIsImporting(true)
+ setImportErrors([])
+
+ try {
+ const result = await importBiddingItemsFromExcel(importFile)
+
+ if (result.errors.length > 0) {
+ setImportErrors(result.errors)
+ toast.warning(
+ `${result.items.length}개의 품목을 파싱했지만 ${result.errors.length}개의 오류가 있습니다.`
+ )
+ return
+ }
+
+ if (result.items.length === 0) {
+ toast.error('가져올 품목이 없습니다.')
+ return
+ }
+
+ // 기존 아이템에 추가
+ setItems((prev) => [...prev, ...result.items])
+ setImportDialogOpen(false)
+ setImportFile(null)
+ setImportErrors([])
+ toast.success(`${result.items.length}개의 품목이 추가되었습니다.`)
+ } catch (error) {
+ console.error('Excel import error:', error)
+ toast.error('Excel 가져오기 중 오류가 발생했습니다.')
+ } finally {
+ setIsImporting(false)
+ }
+ }
+
if (isLoading) {
return (
<div className="flex items-center justify-center p-8">
@@ -1372,6 +1457,14 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems
<FileText className="h-4 w-4" />
사전견적
</Button>
+ <Button onClick={handleExport} variant="outline" className="flex items-center gap-2" disabled={readonly || isExporting || items.length === 0}>
+ <FileSpreadsheet className="h-4 w-4" />
+ {isExporting ? "내보내는 중..." : "Excel 내보내기"}
+ </Button>
+ <Button onClick={() => setImportDialogOpen(true)} variant="outline" className="flex items-center gap-2" disabled={readonly}>
+ <Upload className="h-4 w-4" />
+ Excel 가져오기
+ </Button>
<Button onClick={handleAddItem} className="flex items-center gap-2" disabled={readonly}>
<Plus className="h-4 w-4" />
품목 추가
@@ -1492,6 +1585,88 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems
toast.success('사전견적용 일반견적이 생성되었습니다')
}}
/>
+
+ {/* Excel 가져오기 다이얼로그 */}
+ <Dialog open={importDialogOpen} onOpenChange={setImportDialogOpen}>
+ <DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
+ <DialogHeader>
+ <DialogTitle>Excel 가져오기</DialogTitle>
+ <DialogDescription>
+ Excel 파일을 업로드하여 품목을 일괄 추가합니다.
+ </DialogDescription>
+ </DialogHeader>
+ <div className="space-y-4">
+ <div>
+ <Label htmlFor="import-file">Excel 파일 선택</Label>
+ <Input
+ id="import-file"
+ type="file"
+ accept=".xlsx,.xls"
+ onChange={handleImportFileSelect}
+ className="mt-2"
+ disabled={isImporting}
+ />
+ {importFile && (
+ <p className="text-sm text-muted-foreground mt-2">
+ 선택된 파일: {importFile.name}
+ </p>
+ )}
+ </div>
+
+ {importErrors.length > 0 && (
+ <div className="space-y-2">
+ <Label className="text-destructive">오류 목록</Label>
+ <div className="max-h-60 overflow-y-auto border rounded-md p-3 bg-destructive/5">
+ <ul className="list-disc list-inside space-y-1">
+ {importErrors.map((error, index) => (
+ <li key={index} className="text-sm text-destructive">
+ {error}
+ </li>
+ ))}
+ </ul>
+ </div>
+ </div>
+ )}
+
+ <div className="text-sm text-muted-foreground space-y-1">
+ <p className="font-semibold">필수 컬럼:</p>
+ <ul className="list-disc list-inside ml-2">
+ <li>자재그룹코드, 자재그룹명</li>
+ <li>수량 또는 중량 (둘 중 하나 필수)</li>
+ <li>수량단위 또는 중량단위</li>
+ <li>납품요청일 (YYYY-MM-DD 형식)</li>
+ <li>내정단가</li>
+ </ul>
+ </div>
+ </div>
+ <DialogFooter>
+ <Button
+ variant="outline"
+ onClick={() => {
+ setImportDialogOpen(false)
+ setImportFile(null)
+ setImportErrors([])
+ }}
+ disabled={isImporting}
+ >
+ 취소
+ </Button>
+ <Button
+ onClick={handleImport}
+ disabled={!importFile || isImporting}
+ >
+ {isImporting ? (
+ <>
+ <RefreshCw className="w-4 h-4 mr-2 animate-spin" />
+ 가져오는 중...
+ </>
+ ) : (
+ "가져오기"
+ )}
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
</div>
)