From 2ce5f9dfbb69f0898c42ab862db5ad142fa24943 Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
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<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>
+    </>
+  )
+}
-- 
cgit v1.2.3

