"use client"; import React, { useEffect, useState, useMemo } from "react"; import { ComponentRendererProps } from "@/types/component"; import { CardDisplayConfig } from "./types"; import { tableTypeApi } from "@/lib/api/screen"; import { filterDOMProps } from "@/lib/utils/domPropsFilter"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; export interface CardDisplayComponentProps extends ComponentRendererProps { config?: CardDisplayConfig; tableData?: any[]; tableColumns?: any[]; } /** * CardDisplay 컴포넌트 * 테이블 데이터를 카드 형태로 표시하는 컴포넌트 */ export const CardDisplayComponent: React.FC = ({ component, isDesignMode = false, isSelected = false, isInteractive = false, onClick, onDragStart, onDragEnd, config, className, style, formData, onFormDataChange, screenId, tableName, tableData = [], tableColumns = [], ...props }) => { // 테이블 데이터 상태 관리 const [loadedTableData, setLoadedTableData] = useState([]); const [loadedTableColumns, setLoadedTableColumns] = useState([]); const [loading, setLoading] = useState(false); // 상세보기 모달 상태 const [viewModalOpen, setViewModalOpen] = useState(false); const [selectedData, setSelectedData] = useState(null); // 편집 모달 상태 const [editModalOpen, setEditModalOpen] = useState(false); const [editData, setEditData] = useState(null); // 카드 액션 핸들러 const handleCardView = (data: any) => { // console.log("👀 상세보기 클릭:", data); setSelectedData(data); setViewModalOpen(true); }; const handleCardEdit = (data: any) => { // console.log("✏️ 편집 클릭:", data); setEditData({ ...data }); // 복사본 생성 setEditModalOpen(true); }; // 편집 폼 데이터 변경 핸들러 const handleEditFormChange = (key: string, value: string) => { setEditData((prev: any) => ({ ...prev, [key]: value })); }; // 편집 저장 핸들러 const handleEditSave = async () => { // console.log("💾 편집 저장:", editData); try { // TODO: 실제 API 호출로 데이터 업데이트 // await tableTypeApi.updateTableData(tableName, editData); // console.log("✅ 편집 저장 완료"); alert("✅ 저장되었습니다!"); // 모달 닫기 setEditModalOpen(false); setEditData(null); // 데이터 새로고침 (필요시) // loadTableData(); } catch (error) { console.error("❌ 편집 저장 실패:", error); alert("❌ 저장에 실패했습니다."); } }; // 테이블 데이터 로딩 useEffect(() => { const loadTableData = async () => { // 디자인 모드에서는 테이블 데이터를 로드하지 않음 if (isDesignMode) { return; } // tableName 확인 (props에서 전달받은 tableName 사용) const tableNameToUse = tableName || component.componentConfig?.tableName || 'user_info'; // 기본 테이블명 설정 if (!tableNameToUse) { // console.log("📋 CardDisplay: 테이블명이 설정되지 않음", { // tableName, // componentTableName: component.componentConfig?.tableName, // }); return; } // console.log("📋 CardDisplay: 사용할 테이블명", { // tableName, // componentTableName: component.componentConfig?.tableName, // finalTableName: tableNameToUse, // }); try { setLoading(true); // console.log(`📋 CardDisplay: ${tableNameToUse} 테이블 데이터 로딩 시작`); // 테이블 데이터와 컬럼 정보를 병렬로 로드 const [dataResponse, columnsResponse] = await Promise.all([ tableTypeApi.getTableData(tableNameToUse, { page: 1, size: 50, // 카드 표시용으로 적당한 개수 }), tableTypeApi.getColumns(tableNameToUse), ]); // console.log(`📋 CardDisplay: ${tableNameToUse} 데이터 로딩 완료`, { // total: dataResponse.total, // dataLength: dataResponse.data.length, // columnsLength: columnsResponse.length, // sampleData: dataResponse.data.slice(0, 2), // sampleColumns: columnsResponse.slice(0, 3), // }); setLoadedTableData(dataResponse.data); setLoadedTableColumns(columnsResponse); } catch (error) { console.error(`❌ CardDisplay: ${tableNameToUse} 데이터 로딩 실패`, error); setLoadedTableData([]); setLoadedTableColumns([]); } finally { setLoading(false); } }; loadTableData(); }, [isDesignMode, tableName, component.componentConfig?.tableName]); // 컴포넌트 설정 (기본값 보장) const componentConfig = { cardsPerRow: 3, // 기본값 3 (한 행당 카드 수) cardSpacing: 16, cardStyle: { showTitle: true, showSubtitle: true, showDescription: true, showImage: false, showActions: true, maxDescriptionLength: 100, imagePosition: "top", imageSize: "medium", }, columnMapping: {}, dataSource: "table", staticData: [], ...config, ...component.config, ...component.componentConfig, } as CardDisplayConfig; // 컴포넌트 기본 스타일 const componentStyle: React.CSSProperties = { width: "100%", height: "100%", position: "relative", backgroundColor: "transparent", }; // width는 항상 100%로 고정 (부모 컨테이너가 gridColumns로 크기 제어) // 카드 컴포넌트는 ...style 스프레드가 없으므로 여기서 명시적으로 설정 if (isDesignMode) { componentStyle.border = "1px dashed hsl(var(--border))"; componentStyle.borderColor = isSelected ? "hsl(var(--ring))" : "hsl(var(--border))"; } // 표시할 데이터 결정 (로드된 테이블 데이터 우선 사용) const displayData = useMemo(() => { // console.log("📋 CardDisplay: displayData 결정 중", { // dataSource: componentConfig.dataSource, // loadedTableDataLength: loadedTableData.length, // tableDataLength: tableData.length, // staticDataLength: componentConfig.staticData?.length || 0, // }); // 로드된 테이블 데이터가 있으면 항상 우선 사용 (dataSource 설정 무시) if (loadedTableData.length > 0) { // console.log("📋 CardDisplay: 로드된 테이블 데이터 사용", loadedTableData.slice(0, 2)); return loadedTableData; } // props로 전달받은 테이블 데이터가 있으면 사용 if (tableData.length > 0) { // console.log("📋 CardDisplay: props 테이블 데이터 사용", tableData.slice(0, 2)); return tableData; } if (componentConfig.staticData && componentConfig.staticData.length > 0) { // console.log("📋 CardDisplay: 정적 데이터 사용", componentConfig.staticData.slice(0, 2)); return componentConfig.staticData; } // 데이터가 없으면 빈 배열 반환 // console.log("📋 CardDisplay: 표시할 데이터가 없음"); return []; }, [componentConfig.dataSource, loadedTableData, tableData, componentConfig.staticData]); // 실제 사용할 테이블 컬럼 정보 (로드된 컬럼 우선 사용) const actualTableColumns = loadedTableColumns.length > 0 ? loadedTableColumns : tableColumns; // 로딩 중인 경우 로딩 표시 if (loading) { return (
테이블 데이터를 로드하는 중...
); } // 컨테이너 스타일 - 통일된 디자인 시스템 적용 const containerStyle: React.CSSProperties = { display: "grid", gridTemplateColumns: `repeat(${componentConfig.cardsPerRow || 3}, 1fr)`, // 기본값 3 (한 행당 카드 수) gridAutoRows: "min-content", // 자동 행 생성으로 모든 데이터 표시 gap: `${componentConfig.cardSpacing || 32}px`, // 간격 대폭 증가로 여유로운 느낌 padding: "32px", // 패딩 대폭 증가 width: "100%", height: "100%", background: "#f8fafc", // 연한 하늘색 배경 (채도 낮춤) overflow: "auto", borderRadius: "12px", // 컨테이너 자체도 라운드 처리 }; // 카드 스타일 - 통일된 디자인 시스템 적용 const cardStyle: React.CSSProperties = { backgroundColor: "white", border: "2px solid #e5e7eb", // 더 명확한 테두리 borderRadius: "12px", // 통일된 라운드 처리 padding: "24px", // 더 여유로운 패딩 boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)", // 더 깊은 그림자 transition: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)", // 부드러운 트랜지션 overflow: "hidden", display: "flex", flexDirection: "column", position: "relative", minHeight: "240px", // 최소 높이 더 증가 cursor: isDesignMode ? "pointer" : "default", // 호버 효과를 위한 추가 스타일 "&:hover": { transform: "translateY(-2px)", boxShadow: "0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)", borderColor: "#f59e0b", // 호버 시 오렌지 테두리 } }; // 텍스트 자르기 함수 const truncateText = (text: string, maxLength: number) => { if (!text) return ""; if (text.length <= maxLength) return text; return text.substring(0, maxLength) + "..."; }; // 컬럼 매핑에서 값 가져오기 const getColumnValue = (data: any, columnName?: string) => { if (!columnName) return ""; return data[columnName] || ""; }; // 컬럼명을 라벨로 변환하는 헬퍼 함수 const getColumnLabel = (columnName: string) => { if (!actualTableColumns || actualTableColumns.length === 0) return columnName; const column = actualTableColumns.find((col) => col.columnName === columnName); return column?.columnLabel || columnName; }; // 자동 폴백 로직 - 컬럼이 설정되지 않은 경우 적절한 기본값 찾기 const getAutoFallbackValue = (data: any, type: "title" | "subtitle" | "description") => { const keys = Object.keys(data); switch (type) { case "title": // 이름 관련 필드 우선 검색 return data.name || data.title || data.label || data[keys[0]] || "제목 없음"; case "subtitle": // 직책, 부서, 카테고리 관련 필드 검색 return data.position || data.role || data.department || data.category || data.type || ""; case "description": // 설명, 내용 관련 필드 검색 return data.description || data.content || data.summary || data.memo || ""; default: return ""; } }; // 이벤트 핸들러 const handleClick = (e: React.MouseEvent) => { e.stopPropagation(); onClick?.(); }; const handleCardClick = (data: any) => { if (componentConfig.onCardClick) { componentConfig.onCardClick(data); } }; // DOM 안전한 props만 필터링 (filterDOMProps 유틸리티 사용) const safeDomProps = filterDOMProps(props); return ( <>
{displayData.length === 0 ? (
표시할 데이터가 없습니다.
) : ( displayData.map((data, index) => { // 타이틀, 서브타이틀, 설명 값 결정 (원래 카드 레이아웃과 동일한 로직) const titleValue = getColumnValue(data, componentConfig.columnMapping?.titleColumn) || getAutoFallbackValue(data, "title"); const subtitleValue = getColumnValue(data, componentConfig.columnMapping?.subtitleColumn) || getAutoFallbackValue(data, "subtitle"); const descriptionValue = getColumnValue(data, componentConfig.columnMapping?.descriptionColumn) || getAutoFallbackValue(data, "description"); const imageValue = componentConfig.columnMapping?.imageColumn ? getColumnValue(data, componentConfig.columnMapping.imageColumn) : data.avatar || data.image || ""; return (
handleCardClick(data)} > {/* 카드 이미지 - 통일된 디자인 */} {componentConfig.cardStyle?.showImage && componentConfig.columnMapping?.imageColumn && (
👤
)} {/* 카드 타이틀 - 통일된 디자인 */} {componentConfig.cardStyle?.showTitle && (

{titleValue}

)} {/* 카드 서브타이틀 - 통일된 디자인 */} {componentConfig.cardStyle?.showSubtitle && (

{subtitleValue}

)} {/* 카드 설명 - 통일된 디자인 */} {componentConfig.cardStyle?.showDescription && (

{truncateText(descriptionValue, componentConfig.cardStyle?.maxDescriptionLength || 100)}

)} {/* 추가 표시 컬럼들 - 통일된 디자인 */} {componentConfig.columnMapping?.displayColumns && componentConfig.columnMapping.displayColumns.length > 0 && (
{componentConfig.columnMapping.displayColumns.map((columnName, idx) => { const value = getColumnValue(data, columnName); if (!value) return null; return (
{getColumnLabel(columnName)}: {value}
); })}
)} {/* 카드 액션 (선택사항) */}
); }) )}
{/* 상세보기 모달 */} 📋 상세 정보 {selectedData && (
{Object.entries(selectedData) .filter(([key, value]) => value !== null && value !== undefined && value !== '') .map(([key, value]) => (
{key.replace(/_/g, ' ')}
{String(value)}
)) }
)}
{/* 편집 모달 */} ✏️ 데이터 편집 {editData && (
{Object.entries(editData) .filter(([key, value]) => value !== null && value !== undefined) .map(([key, value]) => (
handleEditFormChange(key, e.target.value)} className="w-full" placeholder={`${key} 입력`} />
)) }
)}
); };