From 610d3bccf1cb640e2a21df28d8d2a954c2bf337e Mon Sep 17 00:00:00 2001 From: dujinkim Date: Thu, 5 Jun 2025 01:53:35 +0000 Subject: (대표님) 변경사항 0604 - OCR 관련 및 drizzle generated sqls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/welding/table/ocr-table-columns.tsx | 312 ++++++++++++++++++++++++ lib/welding/table/ocr-table-toolbar-actions.tsx | 297 ++++++++++++++++++++++ lib/welding/table/ocr-table.tsx | 143 +++++++++++ 3 files changed, 752 insertions(+) create mode 100644 lib/welding/table/ocr-table-columns.tsx create mode 100644 lib/welding/table/ocr-table-toolbar-actions.tsx create mode 100644 lib/welding/table/ocr-table.tsx (limited to 'lib/welding/table') diff --git a/lib/welding/table/ocr-table-columns.tsx b/lib/welding/table/ocr-table-columns.tsx new file mode 100644 index 00000000..85830405 --- /dev/null +++ b/lib/welding/table/ocr-table-columns.tsx @@ -0,0 +1,312 @@ +"use client" + +import * as React from "react" +import { type ColumnDef } from "@tanstack/react-table" +import { ArrowUpDown, Copy, MoreHorizontal } from "lucide-react" + +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Checkbox } from "@/components/ui/checkbox" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { DataTableColumnHeader } from "@/components/data-table/data-table-column-header" +import { toast } from "sonner" +import { formatDate } from "@/lib/utils" +import { OcrRow } from "@/db/schema" +import { type DataTableRowAction } from "@/types/table" + +interface GetColumnsProps { + setRowAction: React.Dispatch | null>> +} + +export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef[] { + return [ + // 체크박스 컬럼 + { + id: "select", + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + className="translate-y-0.5" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + aria-label="Select row" + className="translate-y-0.5" + /> + ), + enableSorting: false, + enableHiding: false, + }, + + // Report No 컬럼 + { + accessorKey: "reportNo", + header: ({ column }) => ( + + ), + cell: ({ getValue }) => { + const reportNo = getValue() as string + return ( +
+ + {reportNo || "N/A"} + + +
+ ) + }, + enableSorting: true, + enableHiding: false, + }, + + // No 컬럼 + { + accessorKey: "no", + header: ({ column }) => ( + + ), + cell: ({ getValue }) => { + const no = getValue() as string + return ( +
+ {no || "-"} +
+ ) + }, + enableSorting: true, + }, + + // Identification No 컬럼 + { + accessorKey: "identificationNo", + header: ({ column }) => ( + + ), + cell: ({ getValue }) => { + const identificationNo = getValue() as string + return ( +
+ {identificationNo || "-"} +
+ ) + }, + enableSorting: true, + }, + + // Tag No 컬럼 + { + accessorKey: "tagNo", + header: ({ column }) => ( + + ), + cell: ({ getValue }) => { + const tagNo = getValue() as string + return ( +
+ {tagNo || "-"} +
+ ) + }, + enableSorting: true, + }, + + // Joint No 컬럼 + { + accessorKey: "jointNo", + header: ({ column }) => ( + + ), + cell: ({ getValue }) => { + const jointNo = getValue() as string + return ( +
+ {jointNo || "-"} +
+ ) + }, + enableSorting: true, + }, + + // Joint Type 컬럼 + { + accessorKey: "jointType", + header: ({ column }) => ( + + ), + cell: ({ getValue }) => { + const jointType = getValue() as string + return ( + + {jointType || "N/A"} + + ) + }, + enableSorting: true, + }, + + // Welding Date 컬럼 + { + accessorKey: "weldingDate", + header: ({ column }) => ( + + ), + cell: ({ getValue }) => { + const weldingDate = getValue() as string + return ( +
+ {weldingDate || "-"} +
+ ) + }, + enableSorting: true, + }, + + // Confidence 컬럼 + { + accessorKey: "confidence", + header: ({ column }) => ( + + ), + cell: ({ getValue }) => { + const confidence = parseFloat(getValue() as string) || 0 + const percentage = Math.round(confidence * 100) + + let variant: "default" | "secondary" | "destructive" = "default" + if (percentage < 70) variant = "destructive" + else if (percentage < 90) variant = "secondary" + + return ( + + {percentage}% + + ) + }, + enableSorting: true, + }, + + // Source Table 컬럼 + { + accessorKey: "sourceTable", + header: ({ column }) => ( + + ), + cell: ({ getValue }) => { + const sourceTable = getValue() as number + return ( +
+ + T{sourceTable} + +
+ ) + }, + enableSorting: true, + }, + + // Source Row 컬럼 + { + accessorKey: "sourceRow", + header: ({ column }) => ( + + ), + cell: ({ getValue }) => { + const sourceRow = getValue() as number + return ( +
+ {sourceRow} +
+ ) + }, + enableSorting: true, + }, + + // Created At 컬럼 + { + accessorKey: "createdAt", + header: ({ column }) => ( + + ), + cell: ({ cell }) => { + const date = cell.getValue() as Date + return ( +
+ {formatDate(date)} +
+ ) + }, + enableSorting: true, + }, + + // Actions 컬럼 + { + id: "actions", + cell: ({ row }) => ( + + + + + + { + const rowData = row.original + navigator.clipboard.writeText(JSON.stringify(rowData, null, 2)) + toast.success("Row data copied to clipboard") + }} + > + + + { + setRowAction({ type: "view", row }) + }} + > + View Details + + { + setRowAction({ type: "delete", row }) + }} + className="text-destructive focus:text-destructive" + > + Delete + + + + ), + enableSorting: false, + enableHiding: false, + }, + ] +} \ No newline at end of file diff --git a/lib/welding/table/ocr-table-toolbar-actions.tsx b/lib/welding/table/ocr-table-toolbar-actions.tsx new file mode 100644 index 00000000..001b21cb --- /dev/null +++ b/lib/welding/table/ocr-table-toolbar-actions.tsx @@ -0,0 +1,297 @@ +"use client" + +import * as React from "react" +import { type Table } from "@tanstack/react-table" +import { Download, RefreshCcw, Upload, FileText, Loader2 } 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" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { Progress } from "@/components/ui/progress" +import { Badge } from "@/components/ui/badge" +import { toast } from "sonner" +import { OcrRow } from "@/db/schema" + +interface OcrTableToolbarActionsProps { + table: Table +} + +interface UploadProgress { + stage: string + progress: number + message: string +} + +export function OcrTableToolbarActions({ table }: OcrTableToolbarActionsProps) { + const [isLoading, setIsLoading] = React.useState(false) + const [isUploading, setIsUploading] = React.useState(false) + const [uploadProgress, setUploadProgress] = React.useState(null) + const [isUploadDialogOpen, setIsUploadDialogOpen] = React.useState(false) + const [selectedFile, setSelectedFile] = React.useState(null) + const fileInputRef = React.useRef(null) + + + const handleFileSelect = (event: React.ChangeEvent) => { + const file = event.target.files?.[0] + if (file) { + setSelectedFile(file) + } + } + + const validateFile = (file: File): string | null => { + // 파일 크기 체크 (10MB) + if (file.size > 10 * 1024 * 1024) { + return "File size must be less than 10MB" + } + + // 파일 타입 체크 + const allowedTypes = [ + 'application/pdf', + 'image/jpeg', + '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" + } + + return null + } + + const uploadFile = async () => { + if (!selectedFile) { + toast.error("Please select a file first") + return + } + + const validationError = validateFile(selectedFile) + if (validationError) { + toast.error(validationError) + return + } + + try { + setIsUploading(true) + setUploadProgress({ + stage: "preparing", + progress: 10, + message: "Preparing file upload..." + }) + + const formData = new FormData() + formData.append('file', selectedFile) + + setUploadProgress({ + stage: "uploading", + progress: 30, + message: "Uploading file and processing..." + }) + + const response = await fetch('/api/ocr/enhanced', { + method: 'POST', + body: formData, + }) + + setUploadProgress({ + stage: "processing", + progress: 70, + message: "Analyzing document with OCR..." + }) + + if (!response.ok) { + const errorData = await response.json() + throw new Error(errorData.error || 'OCR processing failed') + } + + const result = await response.json() + + setUploadProgress({ + stage: "saving", + progress: 90, + message: "Saving results to database..." + }) + + if (result.success) { + setUploadProgress({ + stage: "complete", + progress: 100, + message: "OCR processing completed successfully!" + }) + + toast.success( + `OCR completed! Extracted ${result.metadata.totalRows} rows from ${result.metadata.totalTables} tables`, + { + description: result.warnings?.length + ? `Warnings: ${result.warnings.join(', ')}` + : undefined + } + ) + + // 성공 후 다이얼로그 닫기 및 상태 초기화 + setTimeout(() => { + setIsUploadDialogOpen(false) + setSelectedFile(null) + setUploadProgress(null) + if (fileInputRef.current) { + fileInputRef.current.value = '' + } + + // 테이블 새로고침 + window.location.reload() + }, 2000) + + } else { + throw new Error(result.error || 'Unknown error occurred') + } + + } catch (error) { + console.error('Error uploading file:', error) + toast.error( + error instanceof Error + ? error.message + : 'An error occurred while processing the file' + ) + setUploadProgress(null) + } finally { + setIsUploading(false) + } + } + + const resetUpload = () => { + setSelectedFile(null) + setUploadProgress(null) + if (fileInputRef.current) { + fileInputRef.current.value = '' + } + } + + return ( +
+ {/* OCR 업로드 다이얼로그 */} + + + + + + + Upload Document for OCR + + Upload a PDF or image file to extract table data using OCR technology. + + + +
+ {/* 파일 선택 */} +
+ + +

+ Supported formats: PDF, JPG, PNG, TIFF, BMP (Max 10MB) +

+
+ + {/* 선택된 파일 정보 */} + {selectedFile && ( +
+
+ + {selectedFile.name} +
+
+ Size: {(selectedFile.size / 1024 / 1024).toFixed(2)} MB + Type: {selectedFile.type} +
+
+ )} + + {/* 업로드 진행상황 */} + {uploadProgress && ( +
+
+ Processing... + + {uploadProgress.stage} + +
+ +

+ {uploadProgress.message} +

+
+ )} + + {/* 액션 버튼들 */} +
+ + +
+
+
+
+ {/* Export 버튼 */} + +
+ ) +} \ No newline at end of file diff --git a/lib/welding/table/ocr-table.tsx b/lib/welding/table/ocr-table.tsx new file mode 100644 index 00000000..91af1c67 --- /dev/null +++ b/lib/welding/table/ocr-table.tsx @@ -0,0 +1,143 @@ +"use client" + +import * as React from "react" +import type { + DataTableAdvancedFilterField, + DataTableFilterField, + DataTableRowAction, +} from "@/types/table" + +import { useDataTable } from "@/hooks/use-data-table" +import { DataTable } from "@/components/data-table/data-table" +import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar" + +import { OcrTableToolbarActions } from "./ocr-table-toolbar-actions" +import { getColumns } from "./ocr-table-columns" +import { OcrRow } from "@/db/schema" +import { getOcrRows } from "../service" + +interface ItemsTableProps { + promises: Promise< + [ + Awaited>, + ] + > +} + +export function OcrTable({ promises }: ItemsTableProps) { + const [{ data, pageCount }] = + React.use(promises) + + const [rowAction, setRowAction] = + React.useState | null>(null) + + const columns = React.useMemo( + () => getColumns({ setRowAction }), + [setRowAction] + ) + + /** + * This component can render either a faceted filter or a search filter based on the `options` prop. + * + * @prop options - An array of objects, each representing a filter option. If provided, a faceted filter is rendered. If not, a search filter is rendered. + * + * Each `option` object has the following properties: + * @prop {string} label - The label for the filter option. + * @prop {string} value - The value for the filter option. + * @prop {React.ReactNode} [icon] - An optional icon to display next to the label. + * @prop {boolean} [withCount] - An optional boolean to display the count of the filter option. + */ + const filterFields: DataTableFilterField[] = [ + + ] + + /** + * Advanced filter fields for the data table. + * These fields provide more complex filtering options compared to the regular filterFields. + * + * Key differences from regular filterFields: + * 1. More field types: Includes 'text', 'multi-select', 'date', and 'boolean'. + * 2. Enhanced flexibility: Allows for more precise and varied filtering options. + * 3. Used with DataTableAdvancedToolbar: Enables a more sophisticated filtering UI. + * 4. Date and boolean types: Adds support for filtering by date ranges and boolean values. + */ + const advancedFilterFields: DataTableAdvancedFilterField[] = [ + { + id: "reportNo", + label: "Report No", + type: "text", + // group: "Basic Info", + }, + { + id: "no", + label: "No", + type: "text", + // group: "Basic Info", + }, + + + { + id: "identificationNo", + label: "Identification No", + type: "text", + // group: "Metadata",a + }, + { + id: "tagNo", + label: "Tag No", + type: "text", + // group: "Metadata", + }, + { + id: "jointNo", + label: "Joint No", + type: "text", + // group: "Metadata", + }, + { + id: "weldingDate", + label: "Welding Date", + type: "date", + // group: "Metadata", + }, + { + id: "createdAt", + label: "생성일", + type: "date", + // group: "Metadata", + }, + ] + + + const { table } = useDataTable({ + data, + columns, + pageCount, + filterFields, + enablePinning: true, + enableAdvancedFilter: true, + initialState: { + sorting: [{ id: "createdAt", desc: true }], + columnPinning: { right: ["actions"] }, + }, + getRowId: (originalRow) => String(originalRow.id), + shallow: false, + clearOnDefault: true, + }) + + return ( + <> + + + + + + + ) +} -- cgit v1.2.3