ERP-node/frontend/lib/registry/components/table-list/CardModeRenderer.tsx

225 lines
7.3 KiB
TypeScript
Raw Normal View History

"use client";
import React from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Eye, Edit, Trash2, MoreHorizontal } from "lucide-react";
import { CardDisplayConfig, ColumnConfig } from "./types";
interface CardModeRendererProps {
data: Record<string, any>[];
cardConfig: CardDisplayConfig;
visibleColumns: ColumnConfig[];
onRowClick?: (row: Record<string, any>) => void;
onRowSelect?: (row: Record<string, any>, selected: boolean) => void;
selectedRows?: string[];
showActions?: boolean;
}
/**
*
*
*/
export const CardModeRenderer: React.FC<CardModeRendererProps> = ({
data,
cardConfig,
visibleColumns,
onRowClick,
onRowSelect,
selectedRows = [],
showActions = true,
}) => {
// 기본값 설정
const config = {
cardsPerRow: 3,
cardSpacing: 16,
showActions: true,
cardHeight: "auto",
...cardConfig,
};
// 카드 그리드 스타일 계산
const gridStyle: React.CSSProperties = {
display: "grid",
gridTemplateColumns: `repeat(${config.cardsPerRow}, 1fr)`,
gap: `${config.cardSpacing}px`,
padding: `${config.cardSpacing}px`,
};
// 카드 높이 스타일
const cardStyle: React.CSSProperties = {
height: config.cardHeight === "auto" ? "auto" : `${config.cardHeight}px`,
cursor: onRowClick ? "pointer" : "default",
};
// 컬럼 값 가져오기 함수
const getColumnValue = (row: Record<string, any>, columnName?: string): string => {
if (!columnName || !row) return "";
return String(row[columnName] || "");
};
// 액션 버튼 렌더링
const renderActions = (row: Record<string, any>) => {
if (!showActions || !config.showActions) return null;
return (
<div className="flex items-center justify-end space-x-1 mt-3 pt-3 border-t border-gray-100">
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation();
// 상세보기 액션
}}
>
<Eye className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation();
// 편집 액션
}}
>
<Edit className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation();
// 삭제 액션
}}
>
<Trash2 className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation();
// 더보기 액션
}}
>
<MoreHorizontal className="h-4 w-4" />
</Button>
</div>
);
};
// 데이터가 없는 경우
if (!data || data.length === 0) {
return (
<div className="flex flex-col items-center justify-center py-12 text-center">
<div className="w-16 h-16 bg-gradient-to-br from-slate-100 to-slate-200 rounded-2xl flex items-center justify-center mb-4">
<div className="w-8 h-8 bg-slate-300 rounded-lg"></div>
</div>
<div className="text-sm font-medium text-slate-600 mb-1"> </div>
<div className="text-xs text-slate-400"> </div>
</div>
);
}
return (
<div style={gridStyle} className="w-full">
{data.map((row, index) => {
const idValue = getColumnValue(row, config.idColumn);
const titleValue = getColumnValue(row, config.titleColumn);
const subtitleValue = getColumnValue(row, config.subtitleColumn);
const descriptionValue = getColumnValue(row, config.descriptionColumn);
const imageValue = getColumnValue(row, config.imageColumn);
const isSelected = selectedRows.includes(idValue);
return (
<Card
key={`card-${index}-${idValue}`}
style={cardStyle}
className={`transition-all duration-200 hover:shadow-md ${
isSelected ? "ring-2 ring-blue-500 bg-blue-50/30" : ""
}`}
onClick={() => onRowClick?.(row)}
>
<CardHeader className="pb-3">
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0">
<CardTitle className="text-sm font-medium truncate">
{titleValue || "제목 없음"}
</CardTitle>
{subtitleValue && (
<div className="text-xs text-gray-500 mt-1 truncate">
{subtitleValue}
</div>
)}
</div>
{/* ID 뱃지 */}
{idValue && (
<Badge variant="secondary" className="ml-2 text-xs">
{idValue}
</Badge>
)}
</div>
</CardHeader>
<CardContent className="pt-0">
{/* 이미지 표시 */}
{imageValue && (
<div className="mb-3">
<img
src={imageValue}
alt={titleValue}
className="w-full h-24 object-cover rounded-md bg-gray-100"
onError={(e) => {
const target = e.target as HTMLImageElement;
target.style.display = "none";
}}
/>
</div>
)}
{/* 설명 표시 */}
{descriptionValue && (
<div className="text-xs text-gray-600 line-clamp-2 mb-3">
{descriptionValue}
</div>
)}
{/* 추가 필드들 표시 (선택적) */}
<div className="space-y-1">
{visibleColumns
.filter(col =>
col.columnName !== config.idColumn &&
col.columnName !== config.titleColumn &&
col.columnName !== config.subtitleColumn &&
col.columnName !== config.descriptionColumn &&
col.columnName !== config.imageColumn &&
col.columnName !== "__checkbox__" &&
col.visible
)
.slice(0, 3) // 최대 3개 추가 필드만 표시
.map((col) => {
const value = getColumnValue(row, col.columnName);
if (!value) return null;
return (
<div key={col.columnName} className="flex justify-between items-center text-xs">
<span className="text-gray-500 truncate">{col.displayName}:</span>
<span className="font-medium truncate ml-2">{value}</span>
</div>
);
})}
</div>
{/* 액션 버튼들 */}
{renderActions(row)}
</CardContent>
</Card>
);
})}
</div>
);
};