summaryrefslogtreecommitdiff
path: root/lib/gtc-contract/gtc-clauses/table/import-excel-dialog.tsx
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-07-29 11:48:59 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-07-29 11:48:59 +0000
commit10f90dc68dec42e9a64e081cc0dce6a484447290 (patch)
tree5bc8bb30e03b09a602e7d414d943d0e7f24b1a0f /lib/gtc-contract/gtc-clauses/table/import-excel-dialog.tsx
parent792fb0c21136eededecf52b5b4aa1a252bdc4bfb (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.tsx381
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