Enhance dynamic form service error handling and add pagination to DataGrid component

- Added error handling in DynamicFormService to throw an error if the record to be updated is not found, improving robustness.
- Enhanced DataGrid component with pagination features, including page size input and navigation buttons, to improve data management and user experience.
- Introduced state management for current page and page size, ensuring that data is displayed in a paginated format, making it easier for users to navigate through large datasets.
This commit is contained in:
kjs
2026-03-31 11:18:41 +09:00
parent 2bd99849b4
commit efed63a6cb
2 changed files with 141 additions and 7 deletions

View File

@@ -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) {

View File

@@ -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<Record<string, Set<string>>>({});
// 페이지네이션
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 (
<div className="flex flex-col h-full overflow-auto">
<div className="flex flex-col h-full flex-1 min-h-0">
<div className="flex-1 min-h-0 overflow-auto">
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<Table>
<TableHeader className="sticky top-0 bg-background z-10 shadow-sm">
@@ -481,13 +540,13 @@ export function DataGrid({
...
</TableCell>
</TableRow>
) : processedData.length === 0 ? (
) : paginatedData.length === 0 ? (
<TableRow>
<TableCell colSpan={columns.length + 1} className="text-center py-8 text-muted-foreground">
{emptyMessage}
</TableCell>
</TableRow>
) : processedData.map((row, rowIdx) => (
) : paginatedData.map((row, rowIdx) => (
<TableRow
key={row.id || rowIdx}
className={cn("cursor-pointer",
@@ -520,7 +579,7 @@ export function DataGrid({
/>
</TableCell>
)}
{showRowNumber && !showCheckbox && <TableCell className="text-center text-[10px] text-muted-foreground">{rowIdx + 1}</TableCell>}
{showRowNumber && !showCheckbox && <TableCell className="text-center text-[10px] text-muted-foreground">{pageOffset + rowIdx + 1}</TableCell>}
{columns.map((col) => (
<TableCell
key={col.key}
@@ -540,6 +599,77 @@ export function DataGrid({
</TableBody>
</Table>
</DndContext>
</div>
{/* 페이지네이션 footer */}
{showPagination && (
<div className="flex items-center justify-between border-t px-3 py-2 text-xs text-muted-foreground shrink-0">
{/* 좌측: 데이터 수량 + 페이지 크기 입력 */}
<div className="flex items-center gap-3">
<div className="flex items-center gap-1">
<span></span>
<span className="font-medium text-foreground">{totalItems.toLocaleString()}</span>
<span></span>
</div>
<div className="flex items-center gap-1.5">
<Input
type="number"
min={1}
value={pageSizeInput}
onChange={(e) => setPageSizeInput(e.target.value)}
onBlur={applyPageSize}
onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); applyPageSize(); } }}
className="h-7 w-16 text-center text-xs"
/>
<span> </span>
</div>
</div>
{/* 중앙: 페이지 이동 버튼 */}
<div className="flex items-center gap-0.5">
<button onClick={() => setCurrentPage(1)} disabled={safePage === 1}
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed" title="첫 페이지">
<ChevronsLeft className="h-3.5 w-3.5" />
</button>
<button onClick={() => setCurrentPage((p) => Math.max(1, p - 1))} disabled={safePage === 1}
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed" title="이전 페이지">
<ChevronLeft className="h-3.5 w-3.5" />
</button>
{getPageNumbers().map((page, idx) =>
page === "..." ? (
<span key={`dot-${idx}`} className="h-7 w-7 flex items-center justify-center text-muted-foreground">...</span>
) : (
<button key={page} onClick={() => setCurrentPage(page as number)}
className={cn("h-7 w-7 flex items-center justify-center rounded text-xs",
page === safePage ? "bg-primary text-primary-foreground font-medium" : "hover:bg-muted")}>
{page}
</button>
)
)}
<button onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))} disabled={safePage === totalPages}
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed" title="다음 페이지">
<ChevronRight className="h-3.5 w-3.5" />
</button>
<button onClick={() => setCurrentPage(totalPages)} disabled={safePage === totalPages}
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed" title="마지막 페이지">
<ChevronsRight className="h-3.5 w-3.5" />
</button>
</div>
{/* 우측: 좌측과 균형용 빈 공간 */}
<div className="flex items-center gap-3 invisible">
<div className="flex items-center gap-1">
<span></span>
<span>{totalItems.toLocaleString()}</span>
<span></span>
</div>
<div className="flex items-center gap-1.5">
<div className="h-7 w-16" />
<span> </span>
</div>
</div>
</div>
)}
{/* 이미지 확대 모달 */}
{previewImage && (