summaryrefslogtreecommitdiff
path: root/lib/vendor-pool/table/vendor-pool-excel-import-button.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/vendor-pool/table/vendor-pool-excel-import-button.tsx')
-rw-r--r--lib/vendor-pool/table/vendor-pool-excel-import-button.tsx258
1 files changed, 258 insertions, 0 deletions
diff --git a/lib/vendor-pool/table/vendor-pool-excel-import-button.tsx b/lib/vendor-pool/table/vendor-pool-excel-import-button.tsx
new file mode 100644
index 00000000..704d4aff
--- /dev/null
+++ b/lib/vendor-pool/table/vendor-pool-excel-import-button.tsx
@@ -0,0 +1,258 @@
+/**
+ * 특정 컬럼들 복합키로 묶어 UPDATE 처리해야 함.
+ */
+
+"use client"
+
+import React, { useRef } from 'react'
+import ExcelJS from 'exceljs'
+import { toast } from 'sonner'
+import { Button } from '@/components/ui/button'
+import { Upload, Loader } from 'lucide-react'
+import { createVendorPool } from '../service'
+import { Input } from '@/components/ui/input'
+import { useSession } from "next-auth/react"
+import {
+ getCellValueAsString,
+ parseBoolean,
+ getAccessorKeyByHeader,
+ vendorPoolExcelColumns
+} from '../excel-utils'
+
+interface ImportExcelProps {
+ onSuccess?: () => void
+}
+
+export function ImportVendorPoolButton({ onSuccess }: ImportExcelProps) {
+ const fileInputRef = useRef<HTMLInputElement>(null)
+ const [isImporting, setIsImporting] = React.useState(false)
+ const { data: session, status } = useSession()
+
+ // 헬퍼 함수들은 excel-utils에서 import
+
+ const handleImport = async (event: React.ChangeEvent<HTMLInputElement>) => {
+ const file = event.target.files?.[0]
+ if (!file) return
+
+ setIsImporting(true)
+
+ try {
+ // Read the Excel file using ExcelJS
+ const data = await file.arrayBuffer()
+ const workbook = new ExcelJS.Workbook()
+ await workbook.xlsx.load(data)
+
+ // Get the first worksheet
+ const worksheet = workbook.getWorksheet(1)
+ if (!worksheet) {
+ toast.error("No worksheet found in the spreadsheet")
+ return
+ }
+
+ // Check if there's an instruction row (템플릿 안내 텍스트가 있는지 확인)
+ const firstRowText = getCellValueAsString(worksheet.getRow(1).getCell(1));
+ const hasInstructionRow = firstRowText.includes('벤더풀 데이터 입력 템플릿') ||
+ firstRowText.includes('입력 가이드 시트') ||
+ firstRowText.includes('입력 가이드') ||
+ (worksheet.getRow(1).getCell(1).value !== null &&
+ worksheet.getRow(1).getCell(2).value === null);
+
+ // Get header row index (row 2 if there's an instruction row, otherwise row 1)
+ const headerRowIndex = hasInstructionRow ? 2 : 1;
+
+ // Get column headers and their indices
+ const headerRow = worksheet.getRow(headerRowIndex);
+ const columnIndices: Record<string, number> = {};
+
+ headerRow.eachCell((cell, colNumber) => {
+ const header = getCellValueAsString(cell);
+ // Excel 헤더를 통해 accessorKey 찾기
+ const accessorKey = getAccessorKeyByHeader(header);
+ if (accessorKey) {
+ columnIndices[accessorKey] = colNumber;
+ }
+ });
+
+ // Process data rows
+ const rows: any[] = [];
+ const startRow = headerRowIndex + 1;
+
+ for (let i = startRow; i <= worksheet.rowCount; i++) {
+ const row = worksheet.getRow(i);
+
+ // Skip empty rows
+ if (row.cellCount === 0) continue;
+
+ // Check if this is likely an empty template row (빈 템플릿 행 건너뛰기)
+ let hasAnyData = false;
+ for (let col = 1; col <= row.cellCount; col++) {
+ if (getCellValueAsString(row.getCell(col)).trim()) {
+ hasAnyData = true;
+ break;
+ }
+ }
+ if (!hasAnyData) continue;
+
+ const rowData: Record<string, any> = {};
+ let hasData = false;
+
+ // Map the data using accessorKey indices
+ Object.entries(columnIndices).forEach(([accessorKey, colIndex]) => {
+ const value = getCellValueAsString(row.getCell(colIndex));
+ if (value) {
+ rowData[accessorKey] = value;
+ hasData = true;
+ }
+ });
+
+ if (hasData) {
+ rows.push(rowData);
+ }
+ }
+
+ if (rows.length === 0) {
+ toast.error("No data found in the spreadsheet")
+ setIsImporting(false)
+ return
+ }
+
+ // Process each row
+ let successCount = 0;
+ let errorCount = 0;
+
+ // Create promises for all vendor pool creation operations
+ const promises = rows.map(async (row) => {
+ try {
+ // Excel 컬럼 설정을 기반으로 데이터 매핑
+ const vendorPoolData: any = {};
+
+ vendorPoolExcelColumns.forEach(column => {
+ const { accessorKey, type } = column;
+ const value = row[accessorKey] || '';
+
+ if (type === 'boolean') {
+ vendorPoolData[accessorKey] = parseBoolean(String(value));
+ } else if (value === '') {
+ // 빈 문자열은 null로 설정 (스키마에 맞게)
+ vendorPoolData[accessorKey] = null;
+ } else {
+ vendorPoolData[accessorKey] = String(value);
+ }
+ });
+
+ // 현재 사용자 정보 추가
+ vendorPoolData.registrant = session?.user?.name || 'system';
+ vendorPoolData.lastModifier = session?.user?.name || 'system';
+
+ // Validate required fields
+ if (!vendorPoolData.constructionSector || !vendorPoolData.htDivision ||
+ !vendorPoolData.designCategory || !vendorPoolData.vendorName ||
+ !vendorPoolData.designCategoryCode || !vendorPoolData.equipBulkDivision) {
+ console.error("Missing required fields", vendorPoolData);
+ errorCount++;
+ return null;
+ }
+
+ // Validate field lengths and formats
+ const validationErrors: string[] = [];
+
+ if (vendorPoolData.designCategoryCode && vendorPoolData.designCategoryCode.length > 2) {
+ validationErrors.push(`설계기능코드는 2자리 이하여야 합니다: ${vendorPoolData.designCategoryCode}`);
+ }
+
+ if (vendorPoolData.equipBulkDivision && vendorPoolData.equipBulkDivision.length > 1) {
+ validationErrors.push(`Equip/Bulk 구분은 1자리여야 합니다: ${vendorPoolData.equipBulkDivision}`);
+ }
+
+ if (vendorPoolData.constructionSector && !['조선', '해양'].includes(vendorPoolData.constructionSector)) {
+ validationErrors.push(`공사부문은 '조선' 또는 '해양'이어야 합니다: ${vendorPoolData.constructionSector}`);
+ }
+
+ if (vendorPoolData.htDivision && !['H', 'T', '공통'].includes(vendorPoolData.htDivision)) {
+ validationErrors.push(`H/T구분은 'H', 'T' 또는 '공통'이어야 합니다: ${vendorPoolData.htDivision}`);
+ }
+
+ if (validationErrors.length > 0) {
+ console.error("Validation errors:", validationErrors, vendorPoolData);
+ errorCount++;
+ return null;
+ }
+
+ if (!session || !session.user || !session.user.id) {
+ toast.error("인증 오류. 로그인 정보를 찾을 수 없습니다.")
+ return
+ }
+
+ // Create the vendor pool entry
+ const result = await createVendorPool(vendorPoolData as any)
+
+ if (!result) {
+ console.error(`Failed to import row - createVendorPool returned null:`, vendorPoolData);
+ errorCount++;
+ return null;
+ }
+
+ successCount++;
+ return result;
+ } catch (error) {
+ console.error("Error processing row:", error, row);
+ errorCount++;
+ return null;
+ }
+ });
+
+ // Wait for all operations to complete
+ await Promise.all(promises);
+
+ // Show results
+ if (successCount > 0) {
+ toast.success(`${successCount}개 항목이 성공적으로 가져와졌습니다.`);
+ if (errorCount > 0) {
+ toast.warning(`${errorCount}개 항목 가져오기에 실패했습니다. 콘솔에서 자세한 오류를 확인하세요.`);
+ }
+ // Call the success callback to refresh data
+ onSuccess?.();
+ } else if (errorCount > 0) {
+ toast.error(`모든 ${errorCount}개 항목 가져오기에 실패했습니다. 데이터 형식을 확인하세요.`);
+ }
+
+ } catch (error) {
+ console.error("Import error:", error);
+ toast.error("Error importing data. Please check file format.");
+ } finally {
+ setIsImporting(false);
+ // Reset the file input
+ if (fileInputRef.current) {
+ fileInputRef.current.value = '';
+ }
+ }
+ }
+
+ return (
+ <>
+ <Input
+ type="file"
+ ref={fileInputRef}
+ onChange={handleImport}
+ accept=".xlsx,.xls"
+ className="hidden"
+ />
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => fileInputRef.current?.click()}
+ disabled={isImporting}
+ className="gap-2"
+ >
+ {isImporting ? (
+ <Loader className="size-4 animate-spin" aria-hidden="true" />
+ ) : (
+ <Upload className="size-4" aria-hidden="true" />
+ )}
+ <span className="hidden sm:inline">
+ {isImporting ? "Importing..." : "Import"}
+ </span>
+ </Button>
+ </>
+ )
+}