diff options
Diffstat (limited to 'lib/welding')
| -rw-r--r-- | lib/welding/service.ts | 41 | ||||
| -rw-r--r-- | lib/welding/table/exporft-ocr-data.ts | 90 | ||||
| -rw-r--r-- | lib/welding/table/ocr-table-columns.tsx | 1 | ||||
| -rw-r--r-- | lib/welding/table/ocr-table-toolbar-actions.tsx | 128 |
4 files changed, 231 insertions, 29 deletions
diff --git a/lib/welding/service.ts b/lib/welding/service.ts index b3a69c36..feb6272b 100644 --- a/lib/welding/service.ts +++ b/lib/welding/service.ts @@ -7,7 +7,7 @@ import { filterColumns } from "@/lib/filter-columns"; import { tagClasses } from "@/db/schema/vendorData"; import { asc, desc, ilike, inArray, and, gte, lte, not, or, eq } from "drizzle-orm"; import { GetOcrRowSchema, UpdateOcrRowSchema } from "./validation"; -import { ocrRows } from "@/db/schema"; +import { OcrRow, ocrRows, users } from "@/db/schema"; import { countOcrRows, selectOcrRows } from "./repository"; import { getServerSession } from "next-auth/next" import { authOptions } from "@/app/api/auth/[...nextauth]/route" @@ -153,4 +153,43 @@ export async function modifyOcrRow(input: ModifyOcrRowInput) { error: "OCR 행을 업데이트하는 중 오류가 발생했습니다." }; } +} + + +export async function getOcrAllRows(): Promise<OcrRow[]> { + try { + const allRows = await db + .select({ + // ocrRows의 모든 필드 + id: ocrRows.id, + tableId: ocrRows.tableId, + sessionId: ocrRows.sessionId, + rowIndex: ocrRows.rowIndex, + reportNo: ocrRows.reportNo, + no: ocrRows.no, + identificationNo: ocrRows.identificationNo, + tagNo: ocrRows.tagNo, + jointNo: ocrRows.jointNo, + jointType: ocrRows.jointType, + weldingDate: ocrRows.weldingDate, + confidence: ocrRows.confidence, + sourceTable: ocrRows.sourceTable, + sourceRow: ocrRows.sourceRow, + userId: ocrRows.userId, + createdAt: ocrRows.createdAt, + + // users 테이블의 필드 + userName: users.name, + userEmail: users.email, + }) + .from(ocrRows) + .leftJoin(users, eq(ocrRows.userId, users.id)) + .orderBy(desc(ocrRows.createdAt)) + + + return allRows + } catch (error) { + console.error("Error fetching all OCR rows:", error) + throw new Error("Failed to fetch all OCR data") + } }
\ No newline at end of file diff --git a/lib/welding/table/exporft-ocr-data.ts b/lib/welding/table/exporft-ocr-data.ts new file mode 100644 index 00000000..76856808 --- /dev/null +++ b/lib/welding/table/exporft-ocr-data.ts @@ -0,0 +1,90 @@ +// lib/export-ocr-data.ts +import ExcelJS from "exceljs" +import type { OcrRow } from "@/db/schema" + +/** + * OCR 데이터를 엑셀로 내보내는 단순한 함수 + */ +export async function exportOcrDataToExcel( + data: OcrRow[], + filename: string = "OCR Data" +): Promise<void> { + if (!data || data.length === 0) { + throw new Error("No data to export") + } + + // 워크북 생성 + const workbook = new ExcelJS.Workbook() + const worksheet = workbook.addWorksheet("OCR Data") + + // 컬럼 정의 (OCR 데이터에 맞게 설정) + const columns = [ + { key: "reportNo", header: "Report No", width: 15 }, + { key: "no", header: "No", width: 10 }, + { key: "identificationNo", header: "Identification No", width: 20 }, + { key: "tagNo", header: "Tag No", width: 15 }, + { key: "jointNo", header: "Joint No", width: 15 }, + { key: "jointType", header: "Joint Type", width: 15 }, + { key: "weldingDate", header: "Welding Date", width: 15 }, + { key: "confidence", header: "confidence", width: 15 }, + { key: "userName", header: "사용자 이름", width: 15 }, + { key: "userEmail", header: "사용자 이메일", width: 15 }, + { key: "createdAt", header: "생성일", width: 20 }, + // 필요한 다른 컬럼들도 추가 가능 + ] + + // 워크시트에 컬럼 설정 + worksheet.columns = columns + + // 헤더 스타일 설정 + const headerRow = worksheet.getRow(1) + headerRow.font = { bold: true } + headerRow.alignment = { horizontal: "center" } + headerRow.fill = { + type: "pattern", + pattern: "solid", + fgColor: { argb: "FFCCCCCC" }, + } + + // 데이터 추가 + data.forEach((row) => { + worksheet.addRow({ + reportNo: row.reportNo || "", + no: row.no || "", + identificationNo: row.identificationNo || "", + tagNo: row.tagNo || "", + jointNo: row.jointNo || "", + jointType: row.jointType || "", + confidence: row.confidence || "", + weldingDate: row.weldingDate ? new Date(row.weldingDate).toLocaleDateString() : "", + userName: row.userName || "", + userEmail: row.userEmail || "", + + createdAt: row.createdAt ? new Date(row.createdAt).toLocaleString() : "", + }) + }) + + // 모든 행에 테두리 추가 + worksheet.eachRow((row, rowNumber) => { + row.eachCell((cell) => { + cell.border = { + top: { style: "thin" }, + left: { style: "thin" }, + bottom: { style: "thin" }, + right: { style: "thin" }, + } + }) + }) + + // 파일 다운로드 + const buffer = await workbook.xlsx.writeBuffer() + const blob = new Blob([buffer], { + type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + }) + const url = URL.createObjectURL(blob) + const link = document.createElement("a") + link.href = url + link.download = `${filename}.xlsx` + link.click() + URL.revokeObjectURL(url) +}
\ No newline at end of file diff --git a/lib/welding/table/ocr-table-columns.tsx b/lib/welding/table/ocr-table-columns.tsx index d1aefe06..93ee1004 100644 --- a/lib/welding/table/ocr-table-columns.tsx +++ b/lib/welding/table/ocr-table-columns.tsx @@ -265,6 +265,7 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<OcrRow> ) }, enableSorting: true, + size:100 }, // Created At 컬럼 { diff --git a/lib/welding/table/ocr-table-toolbar-actions.tsx b/lib/welding/table/ocr-table-toolbar-actions.tsx index 6c6d0637..120ff54f 100644 --- a/lib/welding/table/ocr-table-toolbar-actions.tsx +++ b/lib/welding/table/ocr-table-toolbar-actions.tsx @@ -2,9 +2,8 @@ import * as React from "react" import { type Table } from "@tanstack/react-table" -import { Download, RefreshCcw, Upload, FileText, Loader2 } from "lucide-react" +import { Download, RefreshCcw, Upload, FileText, Loader2, ChevronDown } from "lucide-react" -import { exportTableToExcel } from "@/lib/export" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" @@ -20,6 +19,16 @@ import { Progress } from "@/components/ui/progress" import { Badge } from "@/components/ui/badge" import { toast } from "sonner" import { OcrRow } from "@/db/schema" +import { exportTableToExcel } from "@/lib/export_all" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { getOcrAllRows } from "../service" +import { exportOcrDataToExcel } from "./exporft-ocr-data" interface OcrTableToolbarActionsProps { table: Table<OcrRow> @@ -38,6 +47,7 @@ export function OcrTableToolbarActions({ table }: OcrTableToolbarActionsProps) { const [isUploadDialogOpen, setIsUploadDialogOpen] = React.useState(false) const [selectedFile, setSelectedFile] = React.useState<File | null>(null) const fileInputRef = React.useRef<HTMLInputElement>(null) + const [isExporting, setIsExporting] = React.useState(false) // 다이얼로그 닫기 핸들러 - 업로드 중에는 닫기 방지 const handleDialogOpenChange = (open: boolean) => { @@ -50,11 +60,11 @@ export function OcrTableToolbarActions({ table }: OcrTableToolbarActionsProps) { }) return // 다이얼로그를 닫지 않음 } - + // 업로드가 진행 중이 아니거나 완료되었으면 초기화 후 닫기 resetUpload() } - + setIsUploadDialogOpen(open) } @@ -75,12 +85,12 @@ export function OcrTableToolbarActions({ table }: OcrTableToolbarActionsProps) { const allowedTypes = [ 'application/pdf', 'image/jpeg', - 'image/jpg', + 'image/jpg', 'image/png', 'image/tiff', 'image/bmp' ] - + if (!allowedTypes.includes(file.type)) { return "Only PDF and image files (JPG, PNG, TIFF, BMP) are supported" } @@ -151,8 +161,8 @@ export function OcrTableToolbarActions({ table }: OcrTableToolbarActionsProps) { toast.success( `OCR completed! Extracted ${result.metadata.totalRows} rows from ${result.metadata.totalTables} tables`, { - description: result.warnings?.length - ? `Warnings: ${result.warnings.join(', ')}` + description: result.warnings?.length + ? `Warnings: ${result.warnings.join(', ')}` : undefined } ) @@ -161,7 +171,7 @@ export function OcrTableToolbarActions({ table }: OcrTableToolbarActionsProps) { setTimeout(() => { setIsUploadDialogOpen(false) resetUpload() - + // 테이블 새로고침 window.location.reload() }, 2000) @@ -205,6 +215,50 @@ export function OcrTableToolbarActions({ table }: OcrTableToolbarActionsProps) { } } + // 현재 페이지 데이터만 내보내기 + const exportCurrentPage = () => { + exportTableToExcel(table, { + filename: "OCR Result (Current Page)", + excludeColumns: ["select", "actions"], + }) + } + + // 전체 데이터 내보내기 + const exportAllData = async () => { + if (isExporting) return + + setIsExporting(true) + + try { + toast.loading("전체 데이터를 가져오는 중...", { + description: "잠시만 기다려주세요." + }) + + // 모든 데이터 가져오기 + const allData = await getOcrAllRows() + + toast.dismiss() + + if (allData.length === 0) { + toast.warning("내보낼 데이터가 없습니다.") + return + } + + console.log(allData) + + // 새로운 단순한 export 함수 사용 + await exportOcrDataToExcel(allData, `OCR Result (All Data - ${allData.length} rows)`) + + toast.success(`전체 데이터 ${allData.length}개 행이 성공적으로 내보내졌습니다.`) + + } catch (error) { + console.error('Error exporting all data:', error) + toast.error('전체 데이터 내보내기 중 오류가 발생했습니다.') + } finally { + setIsExporting(false) + } + } + return ( <div className="flex items-center gap-2"> {/* OCR 업로드 다이얼로그 */} @@ -215,7 +269,7 @@ export function OcrTableToolbarActions({ table }: OcrTableToolbarActionsProps) { <span className="hidden sm:inline">Upload OCR</span> </Button> </DialogTrigger> - <DialogContent + <DialogContent className="sm:max-w-md" // 업로드 중에는 ESC 키로도 닫기 방지 onEscapeKeyDown={(e) => { @@ -241,13 +295,13 @@ export function OcrTableToolbarActions({ table }: OcrTableToolbarActionsProps) { )} </DialogTitle> <DialogDescription> - {isUploading && uploadProgress?.stage !== "complete" + {isUploading && uploadProgress?.stage !== "complete" ? "Processing in progress. Please do not close this dialog." : "Upload a PDF or image file to extract table data using OCR technology." } </DialogDescription> </DialogHeader> - + <div className="space-y-4"> {/* 파일 선택 */} <div className="space-y-2"> @@ -292,7 +346,7 @@ export function OcrTableToolbarActions({ table }: OcrTableToolbarActionsProps) { <p className="text-xs text-muted-foreground"> {uploadProgress.message} </p> - + {/* 진행 중일 때 안내 메시지 */} {isUploading && uploadProgress.stage !== "complete" && ( <div className="flex items-center gap-2 p-2 bg-blue-50 dark:bg-blue-950/20 rounded-md"> @@ -332,22 +386,40 @@ export function OcrTableToolbarActions({ table }: OcrTableToolbarActionsProps) { </div> </DialogContent> </Dialog> - + {/* Export 버튼 */} - <Button - variant="outline" - size="sm" - onClick={() => - exportTableToExcel(table, { - filename: "OCR Result", - excludeColumns: ["select", "actions"], - }) - } - className="gap-2" - > - <Download className="size-4" aria-hidden="true" /> - <span className="hidden sm:inline">Export</span> - </Button> + {/* Export 드롭다운 메뉴 */} + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button + variant="outline" + size="sm" + className="gap-2" + disabled={isExporting} + > + {isExporting ? ( + <Loader2 className="size-4 animate-spin" aria-hidden="true" /> + ) : ( + <Download className="size-4" aria-hidden="true" /> + )} + <span className="hidden sm:inline"> + {isExporting ? "Exporting..." : "Export"} + </span> + <ChevronDown className="size-3" aria-hidden="true" /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end" className="w-[200px]"> + <DropdownMenuItem onClick={exportAllData} disabled={isExporting}> + <Download className="mr-2 size-4" /> + 전체 데이터 내보내기 + </DropdownMenuItem> + <DropdownMenuSeparator /> + <DropdownMenuItem onClick={exportCurrentPage} disabled={isExporting}> + <Download className="mr-2 size-4" /> + 현재 페이지 내보내기 + </DropdownMenuItem> + </DropdownMenuContent> + </DropdownMenu> </div> ) }
\ No newline at end of file |
