refactor(WEB): DataTable 개선 및 회계 상세 컴포넌트 리팩토링

- DataTable 컴포넌트 기능 확장 및 코드 개선
- 회계 상세 컴포넌트(Bill/Deposit/Purchase/Sales/Withdrawal) 리팩토링
- 엑셀 다운로드 유틸리티 개선
- 대시보드 및 각종 리스트 페이지 업데이트
- dashboard_type2 페이지 추가
- 프론트엔드 개선 로드맵 문서 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-02-11 11:03:19 +09:00
parent 0db6302652
commit e14335b635
33 changed files with 1354 additions and 217 deletions

View File

@@ -1,6 +1,6 @@
"use client";
import { ReactNode } from "react";
import { ReactNode, memo, useCallback } from "react";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
@@ -204,6 +204,62 @@ function getAlignClass<T>(column: Column<T>): string {
}
}
// 메모이즈드 행 컴포넌트 — 행 데이터가 변경되지 않으면 리렌더링 스킵
interface DataTableRowProps<T extends object> {
row: T;
rowIndex: number;
columns: Column<T>[];
onRowClick?: (row: T) => void;
hoverable: boolean;
striped: boolean;
compact: boolean;
rowKey: string;
}
function DataTableRowInner<T extends object>({
row,
rowIndex,
columns,
onRowClick,
hoverable,
striped,
compact,
}: DataTableRowProps<T>) {
const handleClick = useCallback(() => {
onRowClick?.(row);
}, [onRowClick, row]);
return (
<TableRow
onClick={onRowClick ? handleClick : undefined}
className={cn(
onRowClick && "cursor-pointer",
hoverable && "hover:bg-muted/50",
striped && rowIndex % 2 === 1 && "bg-muted/20",
compact && "h-10"
)}
>
{columns.map((column) => {
const value = column.key in row ? row[column.key as keyof T] : null;
return (
<TableCell
key={String(column.key)}
className={cn(
getAlignClass(column),
column.className,
compact && "py-2"
)}
>
{renderCell(column, value, row, rowIndex)}
</TableCell>
);
})}
</TableRow>
);
}
const MemoizedDataTableRow = memo(DataTableRowInner) as typeof DataTableRowInner;
export function DataTable<T extends object>({
columns,
data,
@@ -216,6 +272,11 @@ export function DataTable<T extends object>({
hoverable = true,
compact = false
}: DataTableProps<T>) {
const stableOnRowClick = useCallback(
(row: T) => onRowClick?.(row),
[onRowClick]
);
return (
<Card className="hidden md:block">
<CardContent className="p-0">
@@ -252,32 +313,17 @@ export function DataTable<T extends object>({
</TableRow>
) : (
data.map((row, rowIndex) => (
<TableRow
<MemoizedDataTableRow<T>
key={row[keyField] ? String(row[keyField]) : `row-${rowIndex}`}
onClick={() => onRowClick?.(row)}
className={cn(
onRowClick && "cursor-pointer",
hoverable && "hover:bg-muted/50",
striped && rowIndex % 2 === 1 && "bg-muted/20",
compact && "h-10"
)}
>
{columns.map((column) => {
const value = column.key in row ? row[column.key as keyof T] : null;
return (
<TableCell
key={String(column.key)}
className={cn(
getAlignClass(column),
column.className,
compact && "py-2"
)}
>
{renderCell(column, value, row, rowIndex)}
</TableCell>
);
})}
</TableRow>
row={row}
rowIndex={rowIndex}
columns={columns}
onRowClick={onRowClick ? stableOnRowClick : undefined}
hoverable={hoverable}
striped={striped}
compact={compact}
rowKey={row[keyField] ? String(row[keyField]) : `row-${rowIndex}`}
/>
))
)}
</TableBody>