summaryrefslogtreecommitdiff
path: root/lib/tech-vendor-candidates/table/import-button.tsx
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-06-24 01:51:59 +0000
committerjoonhoekim <26rote@gmail.com>2025-06-24 01:51:59 +0000
commit6824e097d768f724cf439b410ccfb1ab9685ac98 (patch)
tree1f297313637878e7a4ad6c89b84d5a2c3e9eb650 /lib/tech-vendor-candidates/table/import-button.tsx
parentf4825dd3853188de4688fb4a56c0f4e847da314b (diff)
parent4e63d8427d26d0d1b366ddc53650e15f3481fc75 (diff)
(merge) 대표님/최겸 작업사항 머지
Diffstat (limited to 'lib/tech-vendor-candidates/table/import-button.tsx')
-rw-r--r--lib/tech-vendor-candidates/table/import-button.tsx233
1 files changed, 233 insertions, 0 deletions
diff --git a/lib/tech-vendor-candidates/table/import-button.tsx b/lib/tech-vendor-candidates/table/import-button.tsx
new file mode 100644
index 00000000..ad1e6862
--- /dev/null
+++ b/lib/tech-vendor-candidates/table/import-button.tsx
@@ -0,0 +1,233 @@
+"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 { createVendorCandidate } from '../service'
+import { Input } from '@/components/ui/input'
+import { useSession } from "next-auth/react" // next-auth 세션 훅 추가
+import { decryptWithServerAction } from '@/components/drm/drmUtils' // DRM 복호화 함수 import
+
+interface ImportExcelProps {
+ onSuccess?: () => void
+}
+
+export function ImportVendorCandidatesButton({ onSuccess }: ImportExcelProps) {
+ const fileInputRef = useRef<HTMLInputElement>(null)
+ const [isImporting, setIsImporting] = React.useState(false)
+ const { data: session } = useSession() // status 제거 (사용하지 않음)
+
+ // Helper function to get cell value as string
+ const getCellValueAsString = (cell: ExcelJS.Cell): string => {
+ if (!cell || cell.value === undefined || cell.value === null) return '';
+
+ if (typeof cell.value === 'string') return cell.value.trim();
+ if (typeof cell.value === 'number') return cell.value.toString();
+
+ // Handle rich text
+ if (typeof cell.value === 'object' && 'richText' in cell.value) {
+ return cell.value.richText.map((rt: { text: string }) => rt.text).join('');
+ }
+
+ // Handle dates
+ if (cell.value instanceof Date) {
+ return cell.value.toISOString().split('T')[0];
+ }
+
+ // Fallback
+ return String(cell.value);
+ }
+
+ const handleImport = async (event: React.ChangeEvent<HTMLInputElement>) => {
+ const file = event.target.files?.[0]
+ if (!file) return
+
+ setIsImporting(true)
+
+ try {
+ // DRM 복호화 시도 (실패시 원본 파일 사용)
+ let data: ArrayBuffer;
+ try {
+ data = await decryptWithServerAction(file);
+ console.log('[Import] DRM 복호화 성공');
+ } catch (decryptError) {
+ console.warn('[Import] DRM 복호화 실패, 원본 파일 사용:', decryptError);
+ data = await file.arrayBuffer();
+ }
+
+ // Read the Excel file using ExcelJS
+ 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 hasInstructionRow = 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 headers: Record<number, string> = {};
+ const columnIndices: Record<string, number> = {};
+
+ headerRow.eachCell((cell, colNumber) => {
+ const header = getCellValueAsString(cell);
+ headers[colNumber] = header;
+ columnIndices[header] = colNumber;
+ });
+
+ // Process data rows
+ const rows: Record<string, string>[] = [];
+ 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 example row
+ const isExample = i === startRow && worksheet.getRow(i + 1).values?.length === 0;
+ if (isExample) continue;
+
+ const rowData: Record<string, string> = {};
+ let hasData = false;
+
+ // Map the data using header indices
+ Object.entries(columnIndices).forEach(([header, colIndex]) => {
+ const value = getCellValueAsString(row.getCell(colIndex));
+ if (value) {
+ rowData[header] = 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 candidate creation operations
+ const promises = rows.map(async (row) => {
+ try {
+ // Map Excel columns to our data model
+ const candidateData = {
+ companyName: String(row['Company Name'] || ''),
+ contactEmail: String(row['Contact Email'] || ''),
+ contactPhone: String(row['Contact Phone'] || ''),
+ taxId: String(row['Tax ID'] || ''),
+ address: String(row['Address'] || ''),
+ country: String(row['Country'] || ''),
+ source: String(row['Source'] || ''),
+ items: String(row['Items'] || ''),
+ remark: String(row['Remark'] || row['Remarks'] || ''),
+ // Default to COLLECTED if not specified
+ status: (row['Status'] || 'COLLECTED') as "COLLECTED" | "INVITED" | "DISCARDED"
+ };
+
+ // Validate required fields
+ if (!candidateData.companyName || !candidateData.source ||
+ !candidateData.items) {
+ console.error("Missing required fields", candidateData);
+ errorCount++;
+ return null;
+ }
+
+ if (!session || !session.user || !session.user.id) {
+ toast.error("인증 오류. 로그인 정보를 찾을 수 없습니다.")
+ return
+ }
+
+ // Create the vendor candidate (userId는 이미 number 타입이므로 변환 불필요)
+ const result = await createVendorCandidate(candidateData)
+
+ if (result.error) {
+ console.error(`Failed to import row: ${result.error}`, candidateData);
+ errorCount++;
+ return null;
+ }
+
+ successCount++;
+ return result.data;
+ } 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(`Successfully imported ${successCount} vendor candidates`);
+ if (errorCount > 0) {
+ toast.warning(`Failed to import ${errorCount} rows due to errors`);
+ }
+ // Call the success callback to refresh data
+ onSuccess?.();
+ } else if (errorCount > 0) {
+ toast.error(`Failed to import all ${errorCount} rows due to errors`);
+ }
+
+ } 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>
+ </>
+ )
+} \ No newline at end of file