From 2ce5f9dfbb69f0898c42ab862db5ad142fa24943 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Tue, 14 Oct 2025 09:14:10 +0000 Subject: (최겸) 구매 입찰 1회성 품목 기준정보 개발(스키마, 테이블, CRUD, 페이지 등) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../import-procurement-items-excel-button.tsx | 247 +++++++++++++++++++++ 1 file changed, 247 insertions(+) create mode 100644 lib/procurement-items/table/import-procurement-items-excel-button.tsx (limited to 'lib/procurement-items/table/import-procurement-items-excel-button.tsx') diff --git a/lib/procurement-items/table/import-procurement-items-excel-button.tsx b/lib/procurement-items/table/import-procurement-items-excel-button.tsx new file mode 100644 index 00000000..6a50909e --- /dev/null +++ b/lib/procurement-items/table/import-procurement-items-excel-button.tsx @@ -0,0 +1,247 @@ +"use client" + +import * as React from "react" +import { Upload } from "lucide-react" +import { toast } from "sonner" +import * as ExcelJS from 'exceljs' + +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Progress } from "@/components/ui/progress" + +interface ImportProcurementItemButtonProps { + onImportSuccess?: () => void +} + +export function ImportProcurementItemButton({ onImportSuccess }: ImportProcurementItemButtonProps) { + const [open, setOpen] = React.useState(false) + const [file, setFile] = React.useState(null) + const [isUploading, setIsUploading] = React.useState(false) + const [progress, setProgress] = React.useState(0) + const [error, setError] = React.useState(null) + const fileInputRef = React.useRef(null) + + // 파일 선택 처리 + const handleFileChange = (e: React.ChangeEvent) => { + const selectedFile = e.target.files?.[0] + if (!selectedFile) return + + if (!selectedFile.name.endsWith('.xlsx') && !selectedFile.name.endsWith('.xls')) { + setError("Excel 파일(.xlsx 또는 .xls)만 가능합니다.") + return + } + + setFile(selectedFile) + setError(null) + } + + // 데이터 가져오기 처리 + const handleImport = async () => { + if (!file) { + setError("가져올 파일을 선택해주세요.") + return + } + + try { + setIsUploading(true) + setProgress(0) + setError(null) + + // 파일을 ArrayBuffer로 변환 + const arrayBuffer = await file.arrayBuffer() + + // ExcelJS 워크북 로드 + const workbook = new ExcelJS.Workbook() + await workbook.xlsx.load(arrayBuffer) + + // 첫 번째 워크시트 가져오기 + const worksheet = workbook.worksheets[0] + if (!worksheet) { + throw new Error("Excel 파일에 워크시트가 없습니다.") + } + + // 헤더 행 찾기 + let headerRowIndex = 1 + let headerRow: ExcelJS.Row | undefined + let headerValues: (string | null)[] = [] + + worksheet.eachRow((row, rowNumber) => { + const values = row.values as (string | null)[] + if (!headerRow && values.some(v => v === "품목코드" || v === "itemCode" || v === "item_code")) { + headerRowIndex = rowNumber + headerRow = row + headerValues = [...values] + } + }) + + if (!headerRow) { + throw new Error("Excel 파일에서 헤더 행을 찾을 수 없습니다.") + } + + // 컬럼 매핑 + const columnMap: { [key: string]: number } = {} + headerValues.forEach((header, index) => { + if (header) { + const normalizedHeader = header.toString().toLowerCase() + if (normalizedHeader.includes("품목코드") || normalizedHeader.includes("itemcode") || normalizedHeader === "item_code") { + columnMap.itemCode = index + } else if (normalizedHeader.includes("품목명") || normalizedHeader.includes("itemname") || normalizedHeader === "item_name") { + columnMap.itemName = index + } else if (normalizedHeader.includes("재질") || normalizedHeader.includes("material")) { + columnMap.material = index + } else if (normalizedHeader.includes("규격") || normalizedHeader.includes("specification")) { + columnMap.specification = index + } else if (normalizedHeader.includes("단위") || normalizedHeader.includes("unit")) { + columnMap.unit = index + } else if (normalizedHeader.includes("활성화") || normalizedHeader.includes("isactive") || normalizedHeader === "is_active") { + columnMap.isActive = index + } + } + }) + + // 필수 컬럼 확인 + if (!columnMap.itemCode || !columnMap.itemName) { + throw new Error("필수 컬럼(품목코드, 품목명)을 찾을 수 없습니다.") + } + + // 데이터 행 처리 + const importData: any[] = [] + let successCount = 0 + let errorCount = 0 + + worksheet.eachRow((row, rowNumber) => { + if (rowNumber <= headerRowIndex) return // 헤더 행 건너뜀 + + const values = row.values as (string | null | undefined)[] + + const itemData = { + itemCode: values[columnMap.itemCode]?.toString().trim(), + itemName: values[columnMap.itemName]?.toString().trim(), + material: values[columnMap.material]?.toString().trim() || null, + specification: values[columnMap.specification]?.toString().trim() || null, + unit: values[columnMap.unit]?.toString().trim() || null, + isActive: values[columnMap.isActive]?.toString().trim() || 'Y', + } + + // 필수 필드 검증 + if (!itemData.itemCode || !itemData.itemName) { + errorCount++ + return + } + + importData.push(itemData) + }) + + if (importData.length === 0) { + throw new Error("가져올 데이터가 없습니다.") + } + + setProgress(50) + + // 실제 데이터 저장 처리 (서버 액션 호출) + const { importProcurementItemsFromExcel } = await import('../service') + const result = await importProcurementItemsFromExcel(importData) + + if (!result.success) { + throw new Error(result.message || '가져오기에 실패했습니다.') + } + + setProgress(100) + + toast.success(`${result.importedCount}개 품목이 성공적으로 가져오기를 완료했습니다.`) + + // 성공 콜백 호출 + onImportSuccess?.() + setOpen(false) + + } catch (error) { + console.error('가져오기 오류:', error) + setError(error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.') + toast.error('가져오기에 실패했습니다.') + } finally { + setIsUploading(false) + setProgress(0) + } + } + + return ( + <> + + + + + + 엑셀 파일에서 품목 가져오기 + + 템플릿을 다운로드하여 작성한 후 가져오기를 실행하세요. + + + +
+
+ + + {file && ( +

+ 선택된 파일: {file.name} +

+ )} +
+ + {isUploading && ( +
+ +

+ 처리 중... {progress}% +

+
+ )} + + {error && ( +

{error}

+ )} +
+ + + + + +
+
+ + ) +} -- cgit v1.2.3