summaryrefslogtreecommitdiff
path: root/lib/vendor-pool/table/vendor-pool-excel-import-button.tsx
blob: e07987b36313f3c4aa3b4bade82a572c194d4495 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
/**
 * 특정 컬럼들 복합키로 묶어 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'
import { decryptWithServerAction } from '@/components/drm/drmUtils'

interface ImportExcelProps {
  onSuccess?: () => void
}

export function ImportVendorPoolButton({ onSuccess }: ImportExcelProps) {
  const fileInputRef = useRef<HTMLInputElement>(null)
  const [isImporting, setIsImporting] = React.useState(false)
  const { data: session } = useSession()

  // 헬퍼 함수들은 excel-utils에서 import

  const handleImport = async (event: React.ChangeEvent<HTMLInputElement>) => {
    const file = event.target.files?.[0]
    if (!file) return

    setIsImporting(true)

    try {
      // DRM 복호화 처리
      toast.info("파일을 복호화하고 있습니다...")
      let decryptedData: ArrayBuffer
      
      try {
        decryptedData = await decryptWithServerAction(file)
        toast.success("파일 복호화가 완료되었습니다.")
      } catch (drmError) {
        console.warn("DRM 복호화 실패, 원본 파일로 진행합니다:", drmError)
        toast.warning("DRM 복호화에 실패했습니다. 원본 파일로 진행합니다.")
        decryptedData = await file.arrayBuffer()
      }
      
      // 복호화된 데이터로 ExcelJS 워크북 로드
      toast.info("엑셀 파일을 분석하고 있습니다...")
      const workbook = new ExcelJS.Workbook()
      await workbook.xlsx.load(decryptedData)

      // 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;
      let duplicateErrors: string[] = [];

      // Create promises for all vendor pool creation operations
      const promises = rows.map(async (row) => {
        // Excel 컬럼 설정을 기반으로 데이터 매핑 (catch 블록에서도 사용하기 위해 밖에서 선언)
        const vendorPoolData: any = {};

        try {
          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);

          // Unique 제약 조건 위반 감지 (중복 데이터)
          const errorMessage = error instanceof Error ? error.message : String(error);
          if (errorMessage === 'DUPLICATE_VENDOR_POOL') {
            duplicateErrors.push(`공사부문(${vendorPoolData.constructionSector}), H/T(${vendorPoolData.htDivision}), 자재그룹코드(${vendorPoolData.materialGroupCode}), 협력업체명(${vendorPoolData.vendorName})`);
            // 중복인 경우 에러 카운트를 증가시키지 않고 건너뜀 (전체 import 중단하지 않음)
            return null;
          }

          // 다른 에러의 경우 에러 카운트 증가
          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}개 항목 가져오기에 실패했습니다. 데이터 형식을 확인하세요.`);
      }

      // 중복 데이터가 있었던 경우 개별적으로 표시 (성공/실패와 별개로 처리)
      if (duplicateErrors.length > 0) {
        duplicateErrors.forEach(errorMsg => {
          toast.warning(`중복 데이터로 건너뜀: ${errorMsg}`);
        });
      }

    } 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>
    </>
  )
}