diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-10-14 09:14:10 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-10-14 09:14:10 +0000 |
| commit | 2ce5f9dfbb69f0898c42ab862db5ad142fa24943 (patch) | |
| tree | 64b2d54c5c56860ed36038867c570acd2abf35a3 /lib/procurement-items/table/import-procurement-items-excel-button.tsx | |
| parent | 6acb316af6041f093532a778f66960fc196e1547 (diff) | |
(최겸) 구매 입찰 1회성 품목 기준정보 개발(스키마, 테이블, CRUD, 페이지 등)
Diffstat (limited to 'lib/procurement-items/table/import-procurement-items-excel-button.tsx')
| -rw-r--r-- | lib/procurement-items/table/import-procurement-items-excel-button.tsx | 247 |
1 files changed, 247 insertions, 0 deletions
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<File | null>(null)
+ const [isUploading, setIsUploading] = React.useState(false)
+ const [progress, setProgress] = React.useState(0)
+ const [error, setError] = React.useState<string | null>(null)
+ const fileInputRef = React.useRef<HTMLInputElement>(null)
+
+ // 파일 선택 처리
+ const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ 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 (
+ <>
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => setOpen(true)}
+ >
+ <Upload className="mr-2 h-4 w-4" />
+ 엑셀 가져오기
+ </Button>
+
+ <Dialog open={open} onOpenChange={setOpen}>
+ <DialogContent className="max-w-md">
+ <DialogHeader>
+ <DialogTitle>엑셀 파일에서 품목 가져오기</DialogTitle>
+ <DialogDescription>
+ 템플릿을 다운로드하여 작성한 후 가져오기를 실행하세요.
+ </DialogDescription>
+ </DialogHeader>
+
+ <div className="space-y-4">
+ <div>
+ <input
+ ref={fileInputRef}
+ type="file"
+ accept=".xlsx,.xls"
+ onChange={handleFileChange}
+ className="hidden"
+ />
+ <Button
+ variant="outline"
+ onClick={() => fileInputRef.current?.click()}
+ disabled={isUploading}
+ className="w-full"
+ >
+ 파일 선택
+ </Button>
+ {file && (
+ <p className="mt-2 text-sm text-muted-foreground">
+ 선택된 파일: {file.name}
+ </p>
+ )}
+ </div>
+
+ {isUploading && (
+ <div className="space-y-2">
+ <Progress value={progress} className="w-full" />
+ <p className="text-sm text-muted-foreground">
+ 처리 중... {progress}%
+ </p>
+ </div>
+ )}
+
+ {error && (
+ <p className="text-sm text-destructive">{error}</p>
+ )}
+ </div>
+
+ <DialogFooter>
+ <Button variant="outline" onClick={() => setOpen(false)}>
+ 취소
+ </Button>
+ <Button
+ onClick={handleImport}
+ disabled={!file || isUploading}
+ >
+ {isUploading ? "가져오는 중..." : "가져오기"}
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ </>
+ )
+}
|
