From 14f61e24947fb92dd71ec0a7196a6e815f8e66da Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 21 Jul 2025 07:54:26 +0000 Subject: (최겸)기술영업 RFQ 담당자 초대, 요구사항 반영 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/tech-vendors/table/import-button.tsx | 692 +++++++++++++++++-------------- 1 file changed, 380 insertions(+), 312 deletions(-) (limited to 'lib/tech-vendors/table/import-button.tsx') diff --git a/lib/tech-vendors/table/import-button.tsx b/lib/tech-vendors/table/import-button.tsx index ba01e150..1d3bf242 100644 --- a/lib/tech-vendors/table/import-button.tsx +++ b/lib/tech-vendors/table/import-button.tsx @@ -1,313 +1,381 @@ -"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" -import { importTechVendorsFromExcel } from "../service" -import { decryptWithServerAction } from "@/components/drm/drmUtils" - -interface ImportTechVendorButtonProps { - onSuccess?: () => void; -} - -export function ImportTechVendorButton({ onSuccess }: ImportTechVendorButtonProps) { - const [open, setOpen] = React.useState(false); - const [file, setFile] = React.useState(null); - const [isUploading, setIsUploading] = React.useState(false); - const [progress, setProgress] = React.useState(0); - const [error, setError] = React.useState(null); - - const fileInputRef = React.useRef(null); - - // 파일 선택 처리 - const handleFileChange = (e: React.ChangeEvent) => { - 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); - - // DRM 복호화 처리 - let arrayBuffer: ArrayBuffer; - try { - setProgress(10); - toast.info("파일 복호화 중..."); - arrayBuffer = await decryptWithServerAction(file); - setProgress(30); - } catch (decryptError) { - console.error("파일 복호화 실패, 원본 파일 사용:", decryptError); - toast.warning("파일 복호화에 실패하여 원본 파일을 사용합니다."); - 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 === "vendorName")) { - headerRowIndex = rowNumber; - headerRow = row; - headerValues = [...values]; - } - }); - - if (!headerRow) { - throw new Error("Excel 파일에서 헤더 행을 찾을 수 없습니다."); - } - - // 헤더를 기반으로 인덱스 매핑 생성 - const headerMapping: Record = {}; - headerValues.forEach((value, index) => { - if (typeof value === 'string') { - headerMapping[value] = index; - } - }); - - // 필수 헤더 확인 - const requiredHeaders = ["업체명", "이메일", "사업자등록번호", "벤더타입"]; - const alternativeHeaders = { - "업체명": ["vendorName"], - "업체코드": ["vendorCode"], - "이메일": ["email"], - "사업자등록번호": ["taxId"], - "국가": ["country"], - "영문국가명": ["countryEng"], - "제조국": ["countryFab"], - "대리점명": ["agentName"], - "대리점연락처": ["agentPhone"], - "대리점이메일": ["agentEmail"], - "주소": ["address"], - "전화번호": ["phone"], - "웹사이트": ["website"], - "벤더타입": ["techVendorType"], - "대표자명": ["representativeName"], - "대표자이메일": ["representativeEmail"], - "대표자연락처": ["representativePhone"], - "대표자생년월일": ["representativeBirth"], - "아이템": ["items"] - }; - - // 헤더 매핑 확인 (대체 이름 포함) - const missingHeaders = requiredHeaders.filter(header => { - const alternatives = alternativeHeaders[header as keyof typeof alternativeHeaders] || []; - return !(header in headerMapping) && - !alternatives.some(alt => alt in headerMapping); - }); - - if (missingHeaders.length > 0) { - throw new Error(`다음 필수 헤더가 누락되었습니다: ${missingHeaders.join(", ")}`); - } - - // 데이터 행 추출 - const dataRows: Record[] = []; - - worksheet.eachRow((row, rowNumber) => { - if (rowNumber > headerRowIndex) { - const rowData: Record = {}; - const values = row.values as (string | null | undefined)[]; - - // 헤더 매핑에 따라 데이터 추출 - Object.entries(headerMapping).forEach(([header, index]) => { - rowData[header] = values[index] || ""; - }); - - // 빈 행이 아닌 경우만 추가 - if (Object.values(rowData).some(value => value && value.toString().trim() !== "")) { - dataRows.push(rowData); - } - } - }); - - if (dataRows.length === 0) { - throw new Error("Excel 파일에 가져올 데이터가 없습니다."); - } - - // 진행 상황 업데이트를 위한 콜백 - const updateProgress = (current: number, total: number) => { - const percentage = Math.round((current / total) * 100); - setProgress(percentage); - }; - - // 벤더 데이터 처리 - const vendors = dataRows.map(row => ({ - vendorName: row["업체명"] || row["vendorName"] || "", - vendorCode: row["업체코드"] || row["vendorCode"] || null, - email: row["이메일"] || row["email"] || "", - taxId: row["사업자등록번호"] || row["taxId"] || "", - country: row["국가"] || row["country"] || null, - countryEng: row["영문국가명"] || row["countryEng"] || null, - countryFab: row["제조국"] || row["countryFab"] || null, - agentName: row["대리점명"] || row["agentName"] || null, - agentPhone: row["대리점연락처"] || row["agentPhone"] || null, - agentEmail: row["대리점이메일"] || row["agentEmail"] || null, - address: row["주소"] || row["address"] || null, - phone: row["전화번호"] || row["phone"] || null, - website: row["웹사이트"] || row["website"] || null, - techVendorType: row["벤더타입"] || row["techVendorType"] || "", - representativeName: row["대표자명"] || row["representativeName"] || null, - representativeEmail: row["대표자이메일"] || row["representativeEmail"] || null, - representativePhone: row["대표자연락처"] || row["representativePhone"] || null, - representativeBirth: row["대표자생년월일"] || row["representativeBirth"] || null, - items: row["아이템"] || row["items"] || "" - })); - - // 벤더 데이터 가져오기 실행 - const result = await importTechVendorsFromExcel(vendors); - - if (result.success) { - toast.success(`${vendors.length}개의 기술영업 벤더가 성공적으로 가져와졌습니다.`); - } else { - toast.error(result.error || "벤더 가져오기에 실패했습니다."); - } - - // 상태 초기화 및 다이얼로그 닫기 - setFile(null); - setOpen(false); - - // 성공 콜백 호출 - if (onSuccess) { - onSuccess(); - } - } catch (error) { - console.error("Excel 파일 처리 중 오류 발생:", error); - setError(error instanceof Error ? error.message : "파일 처리 중 오류가 발생했습니다."); - } finally { - setIsUploading(false); - } - }; - - // 다이얼로그 열기/닫기 핸들러 - const handleOpenChange = (newOpen: boolean) => { - if (!newOpen) { - // 닫을 때 상태 초기화 - setFile(null); - setError(null); - setProgress(0); - if (fileInputRef.current) { - fileInputRef.current.value = ""; - } - } - setOpen(newOpen); - }; - - return ( - <> - - - - - - 기술영업 벤더 가져오기 - - 기술영업 벤더를 Excel 파일에서 가져옵니다. -
- 올바른 형식의 Excel 파일(.xlsx)을 업로드하세요. -
-
- -
-
- -
- - {file && ( -
- 선택된 파일: {file.name} ({(file.size / 1024).toFixed(1)} KB) -
- )} - - {isUploading && ( -
- -

- {progress}% 완료 -

-
- )} - - {error && ( -
- {error} -
- )} -
- - - - - -
-
- - ); +"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" +import { importTechVendorsFromExcel } from "../service" +import { decryptWithServerAction } from "@/components/drm/drmUtils" + +interface ImportTechVendorButtonProps { + onSuccess?: () => void; +} + +export function ImportTechVendorButton({ onSuccess }: ImportTechVendorButtonProps) { + const [open, setOpen] = React.useState(false); + const [file, setFile] = React.useState(null); + const [isUploading, setIsUploading] = React.useState(false); + const [progress, setProgress] = React.useState(0); + const [error, setError] = React.useState(null); + + const fileInputRef = React.useRef(null); + + // 파일 선택 처리 + const handleFileChange = (e: React.ChangeEvent) => { + 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); + + // DRM 복호화 처리 + let arrayBuffer: ArrayBuffer; + try { + setProgress(10); + toast.info("파일 복호화 중..."); + arrayBuffer = await decryptWithServerAction(file); + setProgress(30); + } catch (decryptError) { + console.error("파일 복호화 실패, 원본 파일 사용:", decryptError); + toast.warning("파일 복호화에 실패하여 원본 파일을 사용합니다."); + 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 === "vendorName")) { + headerRowIndex = rowNumber; + headerRow = row; + headerValues = [...values]; + } + }); + + if (!headerRow) { + throw new Error("Excel 파일에서 헤더 행을 찾을 수 없습니다."); + } + + // 헤더를 기반으로 인덱스 매핑 생성 + const headerMapping: Record = {}; + headerValues.forEach((value, index) => { + if (typeof value === 'string') { + headerMapping[value] = index; + } + }); + + // 필수 헤더 확인 + const requiredHeaders = ["업체명", "이메일", "사업자등록번호", "벤더타입"]; + const alternativeHeaders = { + "업체명": ["vendorName"], + "업체코드": ["vendorCode"], + "이메일": ["email"], + "사업자등록번호": ["taxId"], + "국가": ["country"], + "영문국가명": ["countryEng"], + "제조국": ["countryFab"], + "에이전트명": ["agentName"], + "에이전트연락처": ["agentPhone"], + "에이전트이메일": ["agentEmail"], + "주소": ["address"], + "전화번호": ["phone"], + "웹사이트": ["website"], + "벤더타입": ["techVendorType"], + "대표자명": ["representativeName"], + "대표자이메일": ["representativeEmail"], + "대표자연락처": ["representativePhone"], + "대표자생년월일": ["representativeBirth"], + "담당자명": ["contactName"], + "담당자직책": ["contactPosition"], + "담당자이메일": ["contactEmail"], + "담당자연락처": ["contactPhone"], + "담당자국가": ["contactCountry"], + "아이템": ["items"] + }; + + // 헤더 매핑 확인 (대체 이름 포함) + const missingHeaders = requiredHeaders.filter(header => { + const alternatives = alternativeHeaders[header as keyof typeof alternativeHeaders] || []; + return !(header in headerMapping) && + !alternatives.some(alt => alt in headerMapping); + }); + + if (missingHeaders.length > 0) { + throw new Error(`다음 필수 헤더가 누락되었습니다: ${missingHeaders.join(", ")}`); + } + + // 데이터 행 추출 + const dataRows: Record[] = []; + + worksheet.eachRow((row, rowNumber) => { + if (rowNumber > headerRowIndex) { + const rowData: Record = {}; + const values = row.values as (string | null | undefined)[]; + + // 헤더 매핑에 따라 데이터 추출 + Object.entries(headerMapping).forEach(([header, index]) => { + rowData[header] = values[index] || ""; + }); + + // 빈 행이 아닌 경우만 추가 + if (Object.values(rowData).some(value => value && value.toString().trim() !== "")) { + dataRows.push(rowData); + } + } + }); + + if (dataRows.length === 0) { + throw new Error("Excel 파일에 가져올 데이터가 없습니다."); + } + + setProgress(70); + + // 벤더 데이터 처리 + const vendors = dataRows.map(row => { + const vendorEmail = row["이메일"] || row["email"] || ""; + const contactName = row["담당자명"] || row["contactName"] || ""; + const contactEmail = row["담당자이메일"] || row["contactEmail"] || ""; + + // 담당자 정보 처리: 담당자가 없으면 벤더 이메일을 기본 담당자로 사용 + const contacts = []; + + if (contactName && contactEmail) { + // 명시적인 담당자가 있는 경우 + contacts.push({ + contactName: contactName, + contactPosition: row["담당자직책"] || row["contactPosition"] || "", + contactEmail: contactEmail, + contactPhone: row["담당자연락처"] || row["contactPhone"] || "", + country: row["담당자국가"] || row["contactCountry"] || null, + isPrimary: true + }); + } else if (vendorEmail) { + // 담당자 정보가 없으면 벤더 정보를 기본 담당자로 사용 + const representativeName = row["대표자명"] || row["representativeName"]; + contacts.push({ + contactName: representativeName || row["업체명"] || row["vendorName"] || "기본 담당자", + contactPosition: "기본 담당자", + contactEmail: vendorEmail, + contactPhone: row["대표자연락처"] || row["representativePhone"] || row["전화번호"] || row["phone"] || "", + country: row["국가"] || row["country"] || null, + isPrimary: true + }); + } + + return { + vendorName: row["업체명"] || row["vendorName"] || "", + vendorCode: row["업체코드"] || row["vendorCode"] || null, + email: vendorEmail, + taxId: row["사업자등록번호"] || row["taxId"] || "", + country: row["국가"] || row["country"] || null, + countryEng: row["영문국가명"] || row["countryEng"] || null, + countryFab: row["제조국"] || row["countryFab"] || null, + agentName: row["에이전트명"] || row["agentName"] || null, + agentPhone: row["에이전트연락처"] || row["agentPhone"] || null, + agentEmail: row["에이전트이메일"] || row["agentEmail"] || null, + address: row["주소"] || row["address"] || null, + phone: row["전화번호"] || row["phone"] || null, + website: row["웹사이트"] || row["website"] || null, + techVendorType: row["벤더타입"] || row["techVendorType"] || "", + representativeName: row["대표자명"] || row["representativeName"] || null, + representativeEmail: row["대표자이메일"] || row["representativeEmail"] || null, + representativePhone: row["대표자연락처"] || row["representativePhone"] || null, + representativeBirth: row["대표자생년월일"] || row["representativeBirth"] || null, + items: row["아이템"] || row["items"] || "", + contacts: contacts + }; + }); + + setProgress(90); + toast.info(`${vendors.length}개 벤더 데이터를 서버로 전송 중...`); + + // 벤더 데이터 가져오기 실행 + const result = await importTechVendorsFromExcel(vendors); + + setProgress(100); + + if (result.success) { + // 상세한 결과 메시지 표시 + if (result.message) { + toast.success(`가져오기 완료: ${result.message}`); + } else { + toast.success(`${vendors.length}개의 기술영업 벤더가 성공적으로 가져와졌습니다.`); + } + + // 스킵된 벤더가 있으면 경고 메시지 추가 + if (result.details?.skipped && result.details.skipped.length > 0) { + setTimeout(() => { + const skippedList = result.details.skipped + .map(item => `${item.vendorName} (${item.email}): ${item.reason}`) + .slice(0, 3) // 최대 3개만 표시 + .join('\n'); + const moreText = result.details.skipped.length > 3 ? `\n... 외 ${result.details.skipped.length - 3}개` : ''; + toast.warning(`중복으로 스킵된 벤더:\n${skippedList}${moreText}`); + }, 1000); + } + + // 오류가 있으면 오류 메시지 추가 + if (result.details?.errors && result.details.errors.length > 0) { + setTimeout(() => { + const errorList = result.details.errors + .map(item => `${item.vendorName} (${item.email}): ${item.error}`) + .slice(0, 3) // 최대 3개만 표시 + .join('\n'); + const moreText = result.details.errors.length > 3 ? `\n... 외 ${result.details.errors.length - 3}개` : ''; + toast.error(`처리 중 오류 발생:\n${errorList}${moreText}`); + }, 2000); + } + } else { + toast.error(result.error || "벤더 가져오기에 실패했습니다."); + } + + // 상태 초기화 및 다이얼로그 닫기 + setFile(null); + setOpen(false); + + // 성공 콜백 호출 + if (onSuccess) { + onSuccess(); + } + } catch (error) { + console.error("Excel 파일 처리 중 오류 발생:", error); + setError(error instanceof Error ? error.message : "파일 처리 중 오류가 발생했습니다."); + } finally { + setIsUploading(false); + } + }; + + // 다이얼로그 열기/닫기 핸들러 + const handleOpenChange = (newOpen: boolean) => { + if (!newOpen) { + // 닫을 때 상태 초기화 + setFile(null); + setError(null); + setProgress(0); + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + } + setOpen(newOpen); + }; + + return ( + <> + + + + + + 기술영업 벤더 가져오기 + + 기술영업 벤더를 Excel 파일에서 가져옵니다. +
+ 올바른 형식의 Excel 파일(.xlsx)을 업로드하세요. +
+
+ +
+
+ +
+ + {file && ( +
+ 선택된 파일: {file.name} ({(file.size / 1024).toFixed(1)} KB) +
+ )} + + {isUploading && ( +
+ +

+ {progress}% 완료 +

+
+ )} + + {error && ( +
+ {error} +
+ )} +
+ + + + + +
+
+ + ); } \ No newline at end of file -- cgit v1.2.3