diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-29 11:48:59 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-29 11:48:59 +0000 |
| commit | 10f90dc68dec42e9a64e081cc0dce6a484447290 (patch) | |
| tree | 5bc8bb30e03b09a602e7d414d943d0e7f24b1a0f /lib/gtc-contract/gtc-clauses/table/import-excel-dialog.tsx | |
| parent | 792fb0c21136eededecf52b5b4aa1a252bdc4bfb (diff) | |
(대표님, 박서영, 최겸) document-list-only, gtc, vendorDocu, docu-list-rule
Diffstat (limited to 'lib/gtc-contract/gtc-clauses/table/import-excel-dialog.tsx')
| -rw-r--r-- | lib/gtc-contract/gtc-clauses/table/import-excel-dialog.tsx | 381 |
1 files changed, 381 insertions, 0 deletions
diff --git a/lib/gtc-contract/gtc-clauses/table/import-excel-dialog.tsx b/lib/gtc-contract/gtc-clauses/table/import-excel-dialog.tsx new file mode 100644 index 00000000..f37566fc --- /dev/null +++ b/lib/gtc-contract/gtc-clauses/table/import-excel-dialog.tsx @@ -0,0 +1,381 @@ +"use client" + +import * as React from "react" +import { Upload, Download, FileText, AlertCircle, CheckCircle2, X } from "lucide-react" + +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { Badge } from "@/components/ui/badge" +import { Alert, AlertDescription } from "@/components/ui/alert" +import { ScrollArea } from "@/components/ui/scroll-area" +import { Separator } from "@/components/ui/separator" + +import { type ExcelColumnDef } from "@/lib/export" +import { downloadExcelTemplate, parseExcelFile } from "./excel-import" +import { type GtcClauseTreeView } from "@/db/schema/gtc" +import { toast } from "@/hooks/use-toast" + +interface ImportExcelDialogProps { + documentId: number + columns: ExcelColumnDef[] + onSuccess?: () => void + onImport?: (data: Partial<GtcClauseTreeView>[]) => Promise<void> + trigger?: React.ReactNode +} + +type ImportStep = "upload" | "preview" | "importing" | "complete" + +export function ImportExcelDialog({ + documentId, + columns, + onSuccess, + onImport, + trigger, +}: ImportExcelDialogProps) { + const [open, setOpen] = React.useState(false) + const [step, setStep] = React.useState<ImportStep>("upload") + const [selectedFile, setSelectedFile] = React.useState<File | null>(null) + const [parsedData, setParsedData] = React.useState<Partial<GtcClauseTreeView>[]>([]) + const [errors, setErrors] = React.useState<string[]>([]) + const [isProcessing, setIsProcessing] = React.useState(false) + const fileInputRef = React.useRef<HTMLInputElement>(null) + + // 다이얼로그 열기/닫기 시 상태 초기화 + const handleOpenChange = (isOpen: boolean) => { + setOpen(isOpen) + if (!isOpen) { + // 다이얼로그 닫을 때 상태 초기화 + setStep("upload") + setSelectedFile(null) + setParsedData([]) + setErrors([]) + setIsProcessing(false) + if (fileInputRef.current) { + fileInputRef.current.value = "" + } + } + } + + // 템플릿 다운로드 + const handleDownloadTemplate = async () => { + try { + await downloadExcelTemplate(columns, { + filename: `gtc-clauses-template-${new Date().toISOString().split('T')[0]}`, + includeExampleData: true, + useGroupHeader: true, + }) + + toast({ + title: "템플릿 다운로드 완료", + description: "Excel 템플릿이 다운로드되었습니다. 템플릿에 데이터를 입력한 후 업로드해주세요.", + }) + } catch (error) { + toast({ + title: "템플릿 다운로드 실패", + description: "템플릿 다운로드 중 오류가 발생했습니다.", + variant: "destructive", + }) + } + } + + // 파일 선택 + const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => { + const file = event.target.files?.[0] + if (file) { + if (file.type !== "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" && + file.type !== "application/vnd.ms-excel") { + toast({ + title: "잘못된 파일 형식", + description: "Excel 파일(.xlsx, .xls)만 업로드할 수 있습니다.", + variant: "destructive", + }) + return + } + setSelectedFile(file) + } + } + + // 파일 파싱 + const handleParseFile = async () => { + if (!selectedFile) return + + setIsProcessing(true) + try { + const result = await parseExcelFile<GtcClauseTreeView>( + selectedFile, + columns, + { + hasGroupHeader: true, + sheetName: "GTC조항템플릿", + } + ) + + setParsedData(result.data) + setErrors(result.errors) + + if (result.errors.length > 0) { + toast({ + title: "파싱 완료 (오류 있음)", + description: `${result.data.length}개의 행을 파싱했지만 ${result.errors.length}개의 오류가 있습니다.`, + variant: "destructive", + }) + } else { + toast({ + title: "파싱 완료", + description: `${result.data.length}개의 행이 성공적으로 파싱되었습니다.`, + }) + } + + setStep("preview") + } catch (error) { + toast({ + title: "파싱 실패", + description: "파일 파싱 중 오류가 발생했습니다.", + variant: "destructive", + }) + } finally { + setIsProcessing(false) + } + } + + // 데이터 가져오기 실행 + const handleImportData = async () => { + if (parsedData.length === 0 || !onImport) return + + setStep("importing") + try { + await onImport(parsedData) + setStep("complete") + + toast({ + title: "가져오기 완료", + description: `${parsedData.length}개의 조항이 성공적으로 가져와졌습니다.`, + }) + + // 성공 콜백 호출 후 잠시 후 다이얼로그 닫기 + setTimeout(() => { + onSuccess?.() + setOpen(false) + }, 2000) + } catch (error) { + toast({ + title: "가져오기 실패", + description: "데이터 가져오기 중 오류가 발생했습니다.", + variant: "destructive", + }) + setStep("preview") + } + } + + const renderUploadStep = () => ( + <div className="space-y-6"> + <div className="text-center"> + <FileText className="mx-auto h-12 w-12 text-muted-foreground" /> + <h3 className="mt-4 text-lg font-semibold">Excel 파일로 조항 가져오기</h3> + <p className="mt-2 text-sm text-muted-foreground"> + 먼저 템플릿을 다운로드하여 데이터를 입력한 후 업로드해주세요. + </p> + </div> + + <div className="space-y-4"> + <div> + <Button + onClick={handleDownloadTemplate} + variant="outline" + className="w-full" + > + <Download className="mr-2 h-4 w-4" /> + Excel 템플릿 다운로드 + </Button> + <p className="mt-2 text-xs text-muted-foreground"> + 템플릿에는 입력 가이드와 예시 데이터가 포함되어 있습니다. + </p> + </div> + + <Separator /> + + <div> + <input + ref={fileInputRef} + type="file" + accept=".xlsx,.xls" + onChange={handleFileSelect} + className="hidden" + /> + <Button + onClick={() => fileInputRef.current?.click()} + variant={selectedFile ? "secondary" : "outline"} + className="w-full" + > + <Upload className="mr-2 h-4 w-4" /> + {selectedFile ? selectedFile.name : "Excel 파일 선택"} + </Button> + </div> + + {selectedFile && ( + <Button + onClick={handleParseFile} + disabled={isProcessing} + className="w-full" + > + {isProcessing ? "파싱 중..." : "파일 분석하기"} + </Button> + )} + </div> + </div> + ) + + const renderPreviewStep = () => ( + <div className="space-y-4"> + <div className="flex items-center justify-between"> + <h3 className="text-lg font-semibold">데이터 미리보기</h3> + <div className="flex items-center gap-2"> + <Badge variant="secondary"> + {parsedData.length}개 행 + </Badge> + {errors.length > 0 && ( + <Badge variant="destructive"> + {errors.length}개 오류 + </Badge> + )} + </div> + </div> + + {errors.length > 0 && ( + <Alert variant="destructive"> + <AlertCircle className="h-4 w-4" /> + <AlertDescription> + <div className="space-y-1"> + <div className="font-medium">다음 오류들을 확인해주세요:</div> + <ul className="list-disc list-inside space-y-1 text-sm"> + {errors.slice(0, 5).map((error, index) => ( + <li key={index}>{error}</li> + ))} + {errors.length > 5 && ( + <li>... 및 {errors.length - 5}개 추가 오류</li> + )} + </ul> + </div> + </AlertDescription> + </Alert> + )} + + <ScrollArea className="h-[300px] border rounded-md"> + <Table> + <TableHeader> + <TableRow> + <TableHead className="w-12">#</TableHead> + <TableHead>채번</TableHead> + <TableHead>소제목</TableHead> + <TableHead>상세항목</TableHead> + <TableHead>분류</TableHead> + <TableHead>상태</TableHead> + </TableRow> + </TableHeader> + <TableBody> + {parsedData.map((item, index) => ( + <TableRow key={index}> + <TableCell>{index + 1}</TableCell> + <TableCell className="font-mono"> + {item.itemNumber || "-"} + </TableCell> + <TableCell className="max-w-[200px] truncate"> + {item.subtitle || "-"} + </TableCell> + <TableCell className="max-w-[300px] truncate"> + {item.content || "-"} + </TableCell> + <TableCell>{item.category || "-"}</TableCell> + <TableCell> + <Badge variant={item.isActive ? "default" : "secondary"}> + {item.isActive ? "활성" : "비활성"} + </Badge> + </TableCell> + </TableRow> + ))} + </TableBody> + </Table> + </ScrollArea> + + <div className="flex gap-2"> + <Button + variant="outline" + onClick={() => setStep("upload")} + className="flex-1" + > + 다시 선택 + </Button> + <Button + onClick={handleImportData} + disabled={parsedData.length === 0 || errors.length > 0} + className="flex-1" + > + {errors.length > 0 ? "오류 수정 후 가져오기" : `${parsedData.length}개 조항 가져오기`} + </Button> + </div> + </div> + ) + + const renderImportingStep = () => ( + <div className="text-center py-8"> + <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto"></div> + <h3 className="mt-4 text-lg font-semibold">가져오는 중...</h3> + <p className="mt-2 text-sm text-muted-foreground"> + {parsedData.length}개의 조항을 데이터베이스에 저장하고 있습니다. + </p> + </div> + ) + + const renderCompleteStep = () => ( + <div className="text-center py-8"> + <CheckCircle2 className="mx-auto h-12 w-12 text-green-500" /> + <h3 className="mt-4 text-lg font-semibold">가져오기 완료!</h3> + <p className="mt-2 text-sm text-muted-foreground"> + {parsedData.length}개의 조항이 성공적으로 가져와졌습니다. + </p> + </div> + ) + + return ( + <Dialog open={open} onOpenChange={handleOpenChange}> + <DialogTrigger asChild> + {trigger || ( + <Button variant="outline" size="sm"> + <Upload className="mr-2 h-4 w-4" /> + Excel에서 가져오기 + </Button> + )} + </DialogTrigger> + <DialogContent className="max-w-4xl max-h-[80vh] overflow-hidden"> + <DialogHeader> + <DialogTitle>Excel에서 조항 가져오기</DialogTitle> + <DialogDescription> + Excel 파일을 사용하여 여러 조항을 한 번에 가져올 수 있습니다. + </DialogDescription> + </DialogHeader> + + <div className="mt-4"> + {step === "upload" && renderUploadStep()} + {step === "preview" && renderPreviewStep()} + {step === "importing" && renderImportingStep()} + {step === "complete" && renderCompleteStep()} + </div> + </DialogContent> + </Dialog> + ) +}
\ No newline at end of file |
