diff options
Diffstat (limited to 'lib/items-tech/table/import-excel-button.tsx')
| -rw-r--r-- | lib/items-tech/table/import-excel-button.tsx | 606 |
1 files changed, 303 insertions, 303 deletions
diff --git a/lib/items-tech/table/import-excel-button.tsx b/lib/items-tech/table/import-excel-button.tsx index 4565c365..f8ba9f6d 100644 --- a/lib/items-tech/table/import-excel-button.tsx +++ b/lib/items-tech/table/import-excel-button.tsx @@ -1,304 +1,304 @@ -"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 { processFileImport } from "./ship/import-item-handler" -import { processTopFileImport } from "./top/import-item-handler" -import { processHullFileImport } from "./hull/import-item-handler" -import { decryptWithServerAction } from "@/components/drm/drmUtils" - - -// 선박 아이템 타입 -type ItemType = "ship" | "top" | "hull"; - -const ITEM_TYPE_NAMES = { - ship: "조선 아이템", - top: "해양 TOP 아이템", - hull: "해양 HULL 아이템", -}; - -interface ImportItemButtonProps { - itemType: ItemType; - onSuccess?: () => void; -} - -export function ImportItemButton({ itemType, onSuccess }: ImportItemButtonProps) { - 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); - - // 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 === "자재 코드" || v === "itemCode" || v === "item_code")) { - headerRowIndex = rowNumber; - headerRow = row; - headerValues = [...values]; - } - }); - - if (!headerRow) { - throw new Error("Excel 파일에서 헤더 행을 찾을 수 없습니다."); - } - - // 헤더를 기반으로 인덱스 매핑 생성 - const headerMapping: Record<string, number> = {}; - headerValues.forEach((value, index) => { - if (typeof value === 'string') { - headerMapping[value] = index; - } - }); - - // 필수 헤더 확인 (타입별 구분) - const requiredHeaders: string[] = ["자재 그룹", "기능(공종)"]; - - const alternativeHeaders = { - "자재 그룹": ["itemCode", "item_code"], - "기능(공종)": ["workType"], - "자재명": ["itemList"], - "자재명(상세)": ["subItemList"] - }; - - // 헤더 매핑 확인 (대체 이름 포함) - 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<string, any>[] = []; - - worksheet.eachRow((row, rowNumber) => { - if (rowNumber > headerRowIndex) { - const rowData: Record<string, any> = {}; - 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); - }; - - // 선택된 타입에 따라 적절한 프로세스 함수 호출 - let result: { successCount: number; errorCount: number; errors?: Array<{ row: number; message: string; itemCode?: string; workType?: string }> }; - if (itemType === "top") { - result = await processTopFileImport(dataRows, updateProgress); - } else if (itemType === "hull") { - result = await processHullFileImport(dataRows, updateProgress); - } else { - result = await processFileImport(dataRows, updateProgress); - } - - toast.success(`${result.successCount}개의 ${ITEM_TYPE_NAMES[itemType]}이(가) 성공적으로 가져와졌습니다.`); - - if (result.errorCount > 0) { - const errorDetails = result.errors?.map((error: { row: number; message: string; itemCode?: string; workType?: string }) => - `행 ${error.row}: ${error.itemCode || '알 수 없음'} (${error.workType || '알 수 없음'}) - ${error.message}` - ).join('\n') || '오류 정보를 가져올 수 없습니다.'; - - console.error('Import 오류 상세:', errorDetails); - toast.error(`${result.errorCount}개의 항목 처리 실패. 콘솔에서 상세 내용을 확인하세요.`); - } - - // 상태 초기화 및 다이얼로그 닫기 - 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 ( - <> - <Button - variant="outline" - size="sm" - className="gap-2" - onClick={() => setOpen(true)} - disabled={isUploading} - > - <Upload className="size-4" aria-hidden="true" /> - <span className="hidden sm:inline">Import</span> - </Button> - - <Dialog open={open} onOpenChange={handleOpenChange}> - <DialogContent className="sm:max-w-[500px]"> - <DialogHeader> - <DialogTitle>{ITEM_TYPE_NAMES[itemType]} 가져오기</DialogTitle> - <DialogDescription> - {ITEM_TYPE_NAMES[itemType]}을 Excel 파일에서 가져옵니다. - <br /> - 올바른 형식의 Excel 파일(.xlsx)을 업로드하세요. - </DialogDescription> - </DialogHeader> - - <div className="space-y-4 py-4"> - <div className="flex items-center gap-4"> - <input - type="file" - ref={fileInputRef} - className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-foreground file:font-medium" - accept=".xlsx,.xls" - onChange={handleFileChange} - disabled={isUploading} - /> - </div> - - {file && ( - <div className="text-sm text-muted-foreground"> - 선택된 파일: <span className="font-medium">{file.name}</span> ({(file.size / 1024).toFixed(1)} KB) - </div> - )} - - {isUploading && ( - <div className="space-y-2"> - <Progress value={progress} /> - <p className="text-sm text-muted-foreground text-center"> - {progress}% 완료 - </p> - </div> - )} - - {error && ( - <div className="text-sm font-medium text-destructive"> - {error} - </div> - )} - </div> - - <DialogFooter> - <Button - variant="outline" - onClick={() => setOpen(false)} - disabled={isUploading} - > - 취소 - </Button> - <Button - onClick={handleImport} - disabled={!file || isUploading} - > - {isUploading ? "처리 중..." : "가져오기"} - </Button> - </DialogFooter> - </DialogContent> - </Dialog> - </> - ); +"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 { processFileImport } from "./ship/import-item-handler"
+import { processTopFileImport } from "./top/import-item-handler"
+import { processHullFileImport } from "./hull/import-item-handler"
+import { decryptWithServerAction } from "@/components/drm/drmUtils"
+
+
+// 선박 아이템 타입
+type ItemType = "ship" | "top" | "hull";
+
+const ITEM_TYPE_NAMES = {
+ ship: "조선 아이템",
+ top: "해양 TOP 아이템",
+ hull: "해양 HULL 아이템",
+};
+
+interface ImportItemButtonProps {
+ itemType: ItemType;
+ onSuccess?: () => void;
+}
+
+export function ImportItemButton({ itemType, onSuccess }: ImportItemButtonProps) {
+ 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);
+
+ // 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 === "자재 코드" || v === "itemCode" || v === "item_code")) {
+ headerRowIndex = rowNumber;
+ headerRow = row;
+ headerValues = [...values];
+ }
+ });
+
+ if (!headerRow) {
+ throw new Error("Excel 파일에서 헤더 행을 찾을 수 없습니다.");
+ }
+
+ // 헤더를 기반으로 인덱스 매핑 생성
+ const headerMapping: Record<string, number> = {};
+ headerValues.forEach((value, index) => {
+ if (typeof value === 'string') {
+ headerMapping[value] = index;
+ }
+ });
+
+ // 필수 헤더 확인 (타입별 구분)
+ const requiredHeaders: string[] = ["자재 그룹", "기능(공종)"];
+
+ const alternativeHeaders = {
+ "자재 그룹": ["itemCode", "item_code"],
+ "기능(공종)": ["workType"],
+ "자재명": ["itemList"],
+ "자재명(상세)": ["subItemList"]
+ };
+
+ // 헤더 매핑 확인 (대체 이름 포함)
+ 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<string, any>[] = [];
+
+ worksheet.eachRow((row, rowNumber) => {
+ if (rowNumber > headerRowIndex) {
+ const rowData: Record<string, any> = {};
+ 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);
+ };
+
+ // 선택된 타입에 따라 적절한 프로세스 함수 호출
+ let result: { successCount: number; errorCount: number; errors?: Array<{ row: number; message: string }> };
+ if (itemType === "top") {
+ result = await processTopFileImport(dataRows, updateProgress);
+ } else if (itemType === "hull") {
+ result = await processHullFileImport(dataRows, updateProgress);
+ } else {
+ result = await processFileImport(dataRows, updateProgress);
+ }
+
+ toast.success(`${result.successCount}개의 ${ITEM_TYPE_NAMES[itemType]}이(가) 성공적으로 가져와졌습니다.`);
+
+ if (result.errorCount > 0) {
+ const errorDetails = result.errors?.map((error: { row: number; message: string; itemCode?: string; workType?: string }) =>
+ `행 ${error.row}: ${error.itemCode || '알 수 없음'} (${error.workType || '알 수 없음'}) - ${error.message}`
+ ).join('\n') || '오류 정보를 가져올 수 없습니다.';
+
+ console.error('Import 오류 상세:', errorDetails);
+ toast.error(`${result.errorCount}개의 항목 처리 실패. 콘솔에서 상세 내용을 확인하세요.`);
+ }
+
+ // 상태 초기화 및 다이얼로그 닫기
+ 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 (
+ <>
+ <Button
+ variant="outline"
+ size="sm"
+ className="gap-2"
+ onClick={() => setOpen(true)}
+ disabled={isUploading}
+ >
+ <Upload className="size-4" aria-hidden="true" />
+ <span className="hidden sm:inline">Import</span>
+ </Button>
+
+ <Dialog open={open} onOpenChange={handleOpenChange}>
+ <DialogContent className="sm:max-w-[500px]">
+ <DialogHeader>
+ <DialogTitle>{ITEM_TYPE_NAMES[itemType]} 가져오기</DialogTitle>
+ <DialogDescription>
+ {ITEM_TYPE_NAMES[itemType]}을 Excel 파일에서 가져옵니다.
+ <br />
+ 올바른 형식의 Excel 파일(.xlsx)을 업로드하세요.
+ </DialogDescription>
+ </DialogHeader>
+
+ <div className="space-y-4 py-4">
+ <div className="flex items-center gap-4">
+ <input
+ type="file"
+ ref={fileInputRef}
+ className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-foreground file:font-medium"
+ accept=".xlsx,.xls"
+ onChange={handleFileChange}
+ disabled={isUploading}
+ />
+ </div>
+
+ {file && (
+ <div className="text-sm text-muted-foreground">
+ 선택된 파일: <span className="font-medium">{file.name}</span> ({(file.size / 1024).toFixed(1)} KB)
+ </div>
+ )}
+
+ {isUploading && (
+ <div className="space-y-2">
+ <Progress value={progress} />
+ <p className="text-sm text-muted-foreground text-center">
+ {progress}% 완료
+ </p>
+ </div>
+ )}
+
+ {error && (
+ <div className="text-sm font-medium text-destructive">
+ {error}
+ </div>
+ )}
+ </div>
+
+ <DialogFooter>
+ <Button
+ variant="outline"
+ onClick={() => setOpen(false)}
+ disabled={isUploading}
+ >
+ 취소
+ </Button>
+ <Button
+ onClick={handleImport}
+ disabled={!file || isUploading}
+ >
+ {isUploading ? "처리 중..." : "가져오기"}
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ </>
+ );
}
\ No newline at end of file |
