221 lines
7.5 KiB
TypeScript
221 lines
7.5 KiB
TypeScript
"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>, index: number, e: React.MouseEvent) => void;
|
|
onRowSelect?: (row: Record<string, any>, selected: boolean) => void;
|
|
selectedRows?: string[];
|
|
}
|
|
|
|
/**
|
|
* 카드 모드 렌더러
|
|
* 테이블 데이터를 카드 형태로 표시
|
|
*/
|
|
export const CardModeRenderer: React.FC<CardModeRendererProps> = ({
|
|
data,
|
|
cardConfig,
|
|
visibleColumns,
|
|
onRowClick,
|
|
selectedRows = [],
|
|
}) => {
|
|
// 기본값과 병합
|
|
const config = {
|
|
idColumn: cardConfig?.idColumn || "",
|
|
titleColumn: cardConfig?.titleColumn || "",
|
|
subtitleColumn: cardConfig?.subtitleColumn,
|
|
descriptionColumn: cardConfig?.descriptionColumn,
|
|
imageColumn: cardConfig?.imageColumn,
|
|
cardsPerRow: cardConfig?.cardsPerRow ?? 3,
|
|
cardSpacing: cardConfig?.cardSpacing ?? 16,
|
|
showActions: cardConfig?.showActions ?? true,
|
|
cardHeight: cardConfig?.cardHeight as number | "auto" | undefined,
|
|
};
|
|
|
|
// 디버깅: cardConfig 확인
|
|
console.log("🃏 CardModeRenderer config:", { cardConfig, mergedConfig: config });
|
|
|
|
// 카드 그리드 스타일 계산
|
|
const gridStyle: React.CSSProperties = {
|
|
display: "grid",
|
|
gridTemplateColumns: `repeat(${config.cardsPerRow}, 1fr)`,
|
|
gap: `${config.cardSpacing}px`,
|
|
padding: `${config.cardSpacing}px`,
|
|
overflow: "auto",
|
|
};
|
|
|
|
// 카드 높이 스타일
|
|
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 (!config.showActions) return null;
|
|
|
|
return (
|
|
<div className="mt-3 flex items-center justify-end space-x-1 border-t border-gray-100 pt-3">
|
|
<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="bg-muted mb-4 flex h-16 w-16 items-center justify-center rounded-2xl">
|
|
<div className="bg-muted-foreground/20 h-8 w-8 rounded-lg"></div>
|
|
</div>
|
|
<div className="text-muted-foreground mb-1 text-sm font-medium">표시할 데이터가 없습니다</div>
|
|
<div className="text-muted-foreground/60 text-xs">조건을 변경하거나 새로운 데이터를 추가해보세요</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 ? "bg-blue-50/30 ring-2 ring-blue-500" : ""
|
|
}`}
|
|
onClick={(e) => onRowClick?.(row, index, e)}
|
|
>
|
|
<CardHeader className="pb-3">
|
|
<div className="flex items-start justify-between">
|
|
<div className="min-w-0 flex-1">
|
|
<CardTitle className="truncate text-sm font-medium">{titleValue || "제목 없음"}</CardTitle>
|
|
{subtitleValue && <div className="mt-1 truncate text-xs text-gray-500">{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="h-24 w-full rounded-md bg-gray-100 object-cover"
|
|
onError={(e) => {
|
|
const target = e.target as HTMLImageElement;
|
|
target.style.display = "none";
|
|
}}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* 설명 표시 */}
|
|
{descriptionValue && <div className="mb-3 line-clamp-2 text-xs text-gray-600">{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 items-center justify-between text-xs">
|
|
<span className="truncate text-gray-500">{col.displayName}:</span>
|
|
<span className="ml-2 truncate font-medium">{value}</span>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* 액션 버튼들 */}
|
|
{renderActions(row)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
};
|