"use client"; /** * CardModeRenderer — shared card grid renderer * * 2026-05-20 table-list / v2-table-list 의 중복 CardModeRenderer 를 흡수. * `variant="v2"` prop 으로 v2 전용 이미지 URL 정규화 (objid → getFilePreviewUrl, * path → getFullImageUrl) 와 이미지 로드 실패 시 fallback DOM 삽입을 분기 처리. * * 이 파일은 legacy table-list / v2-table-list local type files 를 import 하지 않는다. * shared 타입은 `./tableListConfigTypes` 에서 가져온다. */ 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 type { CardDisplayConfig, ColumnConfig } from "./tableListConfigTypes"; import { getFullImageUrl } from "@/lib/api/client"; import { getFilePreviewUrl } from "@/lib/api/file"; export type CardModeVariant = "default" | "v2"; interface CardModeRendererProps { /** 시각/동작 분기 — 기본은 table-list 기존 동작, "v2" 는 v2-table-list 흡수 분기 */ variant?: CardModeVariant; data: Record[]; cardConfig: CardDisplayConfig; visibleColumns: ColumnConfig[]; onRowClick?: (row: Record, index: number, e: React.MouseEvent) => void; onRowSelect?: (row: Record, selected: boolean) => void; selectedRows?: string[]; } /** * 카드 모드 렌더러 * 테이블 데이터를 카드 형태로 표시 */ export const CardModeRenderer: React.FC = ({ variant = "default", data, cardConfig, visibleColumns, onRowClick, selectedRows = [], }) => { const isV2 = variant === "v2"; // 기본값과 병합 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, columnName?: string): string => { if (!columnName || !row) return ""; return String(row[columnName] || ""); }; // 액션 버튼 렌더링 const renderActions = (_row: Record) => { if (!config.showActions) return null; return (
); }; // 데이터가 없는 경우 if (!data || data.length === 0) { return (
표시할 데이터가 없습니다
조건을 변경하거나 새로운 데이터를 추가해보세요
); } return (
{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 ( onRowClick?.(row, index, e)} >
{titleValue || "제목 없음"} {subtitleValue &&
{subtitleValue}
}
{/* ID 뱃지 */} {idValue && ( {idValue} )}
{/* 이미지 표시 */} {imageValue && (
{ // ★ v2 전용: 숫자 objid → getFilePreviewUrl, 그 외 path → getFullImageUrl 정규화 const strValue = String(imageValue); const isObjid = /^\d+$/.test(strValue); return isObjid ? getFilePreviewUrl(strValue) : getFullImageUrl(strValue); })() : imageValue } alt={titleValue} className="h-24 w-full rounded-md bg-muted object-cover" onError={(e) => { const target = e.target as HTMLImageElement; // 이미지 로드 실패 시 폴백 표시 target.style.display = "none"; // ★ v2 전용: 폴백 DOM 삽입 (data-image-fallback). default 는 단순 hide 만. if (!isV2) return; const parent = target.parentElement; if (parent && !parent.querySelector("[data-image-fallback]")) { const fallback = document.createElement("div"); fallback.setAttribute("data-image-fallback", "true"); fallback.className = "flex items-center justify-center h-24 w-full rounded-md bg-muted text-muted-foreground"; fallback.innerHTML = ``; parent.appendChild(fallback); } }} />
)} {/* 설명 표시 */} {descriptionValue &&
{descriptionValue}
} {/* 추가 필드들 표시 (선택적) */}
{(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 (
{col.displayName}: {value}
); })}
{/* 액션 버튼들 */} {renderActions(row)}
); })}
); };