diff --git a/backend-node/src/services/dynamicFormService.ts b/backend-node/src/services/dynamicFormService.ts index bc65822c..3bcf4f3d 100644 --- a/backend-node/src/services/dynamicFormService.ts +++ b/backend-node/src/services/dynamicFormService.ts @@ -1195,6 +1195,10 @@ export class DynamicFormService { const updatedRecord = Array.isArray(result) ? result[0] : result; + if (!updatedRecord) { + throw new Error(`업데이트 대상 레코드를 찾을 수 없습니다. (id: ${id}, 테이블: ${tableName})`); + } + // 🔥 조건부 연결 실행 (UPDATE 트리거) try { if (company_code) { diff --git a/frontend/components/common/DataGrid.tsx b/frontend/components/common/DataGrid.tsx index f122a318..0d717acf 100644 --- a/frontend/components/common/DataGrid.tsx +++ b/frontend/components/common/DataGrid.tsx @@ -22,7 +22,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@ import { Checkbox } from "@/components/ui/checkbox"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Input } from "@/components/ui/input"; -import { Filter, Check, Search, ImageIcon, X } from "lucide-react"; +import { Filter, Check, Search, ImageIcon, X, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from "lucide-react"; import { cn } from "@/lib/utils"; import { toast } from "sonner"; import { apiClient } from "@/lib/api/client"; @@ -66,6 +66,10 @@ export interface DataGridProps { loading?: boolean; onColumnOrderChange?: (columns: DataGridColumn[]) => void; gridId?: string; + /** 페이지네이션 표시 여부 (기본: true) */ + showPagination?: boolean; + /** 초기 페이지 크기 (기본: 50) */ + defaultPageSize?: number; } const fmtNum = (val: any) => { @@ -217,6 +221,8 @@ export function DataGrid({ loading = false, onColumnOrderChange, gridId, + showPagination = true, + defaultPageSize = 50, }: DataGridProps) { const [columns, setColumns] = useState(initialColumns); useEffect(() => { setColumns(initialColumns); }, [initialColumns]); @@ -228,6 +234,11 @@ export function DataGrid({ // 헤더 필터 (컬럼별 선택된 값 Set) const [headerFilters, setHeaderFilters] = useState>>({}); + // 페이지네이션 + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(defaultPageSize); + const [pageSizeInput, setPageSizeInput] = useState(String(defaultPageSize)); + // 인라인 편집 const [editingCell, setEditingCell] = useState<{ rowIdx: number; colKey: string } | null>(null); // 이미지 확대 모달 @@ -340,6 +351,53 @@ export function DataGrid({ return result; }, [data, headerFilters, sortKey, sortDir]); + // 필터/데이터 변경 시 1페이지로 리셋 + useEffect(() => { + setCurrentPage(1); + }, [data, headerFilters]); + + // 페이지네이션 계산 + const totalItems = processedData.length; + const totalPages = Math.max(1, Math.ceil(totalItems / pageSize)); + const safePage = Math.min(currentPage, totalPages); + + useEffect(() => { + if (currentPage > totalPages) setCurrentPage(totalPages); + }, [currentPage, totalPages]); + + const pageOffset = (safePage - 1) * pageSize; + const paginatedData = showPagination + ? processedData.slice(pageOffset, pageOffset + pageSize) + : processedData; + + // 페이지 크기 입력 적용 + const applyPageSize = () => { + const n = parseInt(pageSizeInput, 10); + if (!isNaN(n) && n >= 1) { + setPageSize(n); + setCurrentPage(1); + setPageSizeInput(String(n)); + } else { + setPageSizeInput(String(pageSize)); + } + }; + + // 페이지 번호 배열 생성 + const getPageNumbers = () => { + const delta = 2; + let start = Math.max(1, safePage - delta); + let end = Math.min(totalPages, safePage + delta); + if (end - start < delta * 2) { + if (start === 1) end = Math.min(totalPages, start + delta * 2); + else if (end === totalPages) start = Math.max(1, end - delta * 2); + } + const pages: (number | "...")[] = []; + if (start > 1) { pages.push(1); if (start > 2) pages.push("..."); } + for (let i = start; i <= end; i++) pages.push(i); + if (end < totalPages) { if (end < totalPages - 1) pages.push("..."); pages.push(totalPages); } + return pages; + }; + // 인라인 편집 const startEdit = (rowIdx: number, colKey: string, currentVal: any) => { const col = columns.find((c) => c.key === colKey); @@ -351,7 +409,7 @@ export function DataGrid({ const saveEdit = useCallback(async () => { if (!editingCell) return; const { rowIdx, colKey } = editingCell; - const row = processedData[rowIdx]; + const row = paginatedData[rowIdx]; if (!row) { setEditingCell(null); return; } const originalVal = String(row[colKey] ?? ""); @@ -374,7 +432,7 @@ export function DataGrid({ onCellEdit?.(row.id, colKey, editValue, row); setEditingCell(null); - }, [editingCell, editValue, processedData, tableName, onCellEdit]); + }, [editingCell, editValue, paginatedData, tableName, onCellEdit]); const cancelEdit = () => setEditingCell(null); @@ -441,7 +499,8 @@ export function DataGrid({ }; return ( -
+
+
@@ -481,13 +540,13 @@ export function DataGrid({ 로딩 중... - ) : processedData.length === 0 ? ( + ) : paginatedData.length === 0 ? ( {emptyMessage} - ) : processedData.map((row, rowIdx) => ( + ) : paginatedData.map((row, rowIdx) => ( )} - {showRowNumber && !showCheckbox && {rowIdx + 1}} + {showRowNumber && !showCheckbox && {pageOffset + rowIdx + 1}} {columns.map((col) => (
+
+ + {/* 페이지네이션 footer */} + {showPagination && ( +
+ {/* 좌측: 데이터 수량 + 페이지 크기 입력 */} +
+
+ 전체 + {totalItems.toLocaleString()} + +
+
+ setPageSizeInput(e.target.value)} + onBlur={applyPageSize} + onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); applyPageSize(); } }} + className="h-7 w-16 text-center text-xs" + /> + 건씩 보기 +
+
+ + {/* 중앙: 페이지 이동 버튼 */} +
+ + + {getPageNumbers().map((page, idx) => + page === "..." ? ( + ... + ) : ( + + ) + )} + + +
+ + {/* 우측: 좌측과 균형용 빈 공간 */} +
+
+ 전체 + {totalItems.toLocaleString()} + +
+
+
+ 건씩 보기 +
+
+
+ )} {/* 이미지 확대 모달 */} {previewImage && (