"use client"; import React, { ReactNode } from "react"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { cn } from "@/lib/utils"; // 컬럼 정의 타입 export interface RDVColumn { key: string; label: string; width?: string; render?: (value: any, row: T, index: number) => ReactNode; hideOnMobile?: boolean; className?: string; } // 카드 필드 타입 export interface RDVCardField { label: string; render: (item: T) => ReactNode; hideEmpty?: boolean; } // 메인 Props 타입 export interface ResponsiveDataViewProps { data: T[]; columns: RDVColumn[]; keyExtractor: (item: T) => string; // 로딩/빈 상태 isLoading?: boolean; emptyMessage?: string; skeletonCount?: number; // 카드 설정 (모바일) cardTitle: (item: T) => ReactNode; cardSubtitle?: (item: T) => ReactNode; cardHeaderRight?: (item: T) => ReactNode; cardFields?: RDVCardField[] | ((item: T) => RDVCardField[]); // 액션 (테이블 마지막 컬럼 + 카드 하단) renderActions?: (item: T) => ReactNode; actionsLabel?: string; actionsWidth?: string; // 행 클릭 onRowClick?: (item: T) => void; // 스타일 커스터마이징 tableContainerClassName?: string; cardContainerClassName?: string; } // 중첩 객체에서 키 경로로 값을 꺼내는 헬퍼 function getNestedValue(obj: any, path: string): any { return path.split(".").reduce((acc, key) => acc?.[key], obj); } export function ResponsiveDataView({ data, columns, keyExtractor, isLoading = false, emptyMessage, skeletonCount = 5, cardTitle, cardSubtitle, cardHeaderRight, cardFields, renderActions, actionsLabel, actionsWidth, onRowClick, tableContainerClassName, cardContainerClassName, }: ResponsiveDataViewProps) { // cardFields 미지정 시 columns에서 자동 생성 function resolveCardFields(item: T): RDVCardField[] { if (typeof cardFields === "function") return cardFields(item); if (Array.isArray(cardFields)) return cardFields; return columns .filter((col) => !col.hideOnMobile) .map((col) => ({ label: col.label, render: (row: T) => col.render ? col.render(getNestedValue(row, col.key), row, 0) : String(getNestedValue(row, col.key) ?? "-"), })); } // --- 로딩 스켈레톤 --- if (isLoading) { return ( <> {/* 데스크톱 테이블 스켈레톤 */}
{columns.map((col) => ( {col.label} ))} {renderActions && ( {actionsLabel || "작업"} )} {Array.from({ length: skeletonCount }).map((_, rowIdx) => ( {columns.map((col) => (
))} {renderActions && (
)} ))}
{/* 모바일 카드 스켈레톤 */}
{Array.from({ length: skeletonCount }).map((_, i) => (
{Array.from({ length: 3 }).map((_, j) => (
))}
{renderActions && (
)}
))}
); } // --- 빈 상태 --- if (data.length === 0) { return (
{emptyMessage || "데이터가 없습니다."}
); } // --- 실제 데이터 렌더링 --- return ( <> {/* 데스크톱 테이블 (lg 이상) */}
{columns.map((col) => ( {col.label} ))} {renderActions && ( {actionsLabel || "작업"} )} {data.map((item, index) => ( onRowClick?.(item)} > {columns.map((col) => ( {col.render ? col.render(getNestedValue(item, col.key), item, index) : String(getNestedValue(item, col.key) ?? "-")} ))} {renderActions && (
{renderActions(item)}
)}
))}
{/* 모바일 카드 (lg 미만) */}
{data.map((item) => { const fields = resolveCardFields(item); return (
onRowClick?.(item)} > {/* 카드 헤더 */}

{cardTitle(item)}

{cardSubtitle && (

{cardSubtitle(item)}

)}
{cardHeaderRight && (
{cardHeaderRight(item)}
)}
{/* 카드 필드 */} {fields.length > 0 && (
{fields.map((field, i) => (
{field.label} {field.render(item)}
))}
)} {/* 카드 액션 */} {renderActions && (
{renderActions(item)}
)}
); })}
); }