279 lines
11 KiB
TypeScript
279 lines
11 KiB
TypeScript
"use client";
|
|
|
|
import React from "react";
|
|
import { LayoutRendererProps } from "../BaseLayoutRenderer";
|
|
|
|
/**
|
|
* 카드 레이아웃 컴포넌트
|
|
* 3x2 격자로 구성된 카드 대시보드 레이아웃
|
|
*/
|
|
interface CardLayoutProps extends LayoutRendererProps {
|
|
tableData?: any[]; // 테이블 데이터
|
|
tableColumns?: any[]; // 테이블 컬럼 정보 (라벨 포함)
|
|
}
|
|
|
|
export const CardLayoutLayout: React.FC<CardLayoutProps> = ({
|
|
layout,
|
|
children,
|
|
onUpdateLayout,
|
|
onSelectComponent,
|
|
isDesignMode = false,
|
|
className = "",
|
|
onClick,
|
|
allComponents,
|
|
tableData = [],
|
|
tableColumns = [],
|
|
}) => {
|
|
// 카드 설정 가져오기 (기본값 보장)
|
|
const cardConfig = {
|
|
cardsPerRow: layout.layoutConfig?.card?.cardsPerRow ?? 3,
|
|
cardSpacing: layout.layoutConfig?.card?.cardSpacing ?? 16,
|
|
columnMapping: layout.layoutConfig?.card?.columnMapping || {},
|
|
cardStyle: {
|
|
showTitle: layout.layoutConfig?.card?.cardStyle?.showTitle ?? true,
|
|
showSubtitle: layout.layoutConfig?.card?.cardStyle?.showSubtitle ?? true,
|
|
showDescription: layout.layoutConfig?.card?.cardStyle?.showDescription ?? true,
|
|
showImage: layout.layoutConfig?.card?.cardStyle?.showImage ?? false,
|
|
maxDescriptionLength: layout.layoutConfig?.card?.cardStyle?.maxDescriptionLength ?? 100,
|
|
},
|
|
};
|
|
|
|
// 실제 테이블 데이터 사용 (없으면 샘플 데이터)
|
|
const displayData =
|
|
tableData.length > 0
|
|
? tableData
|
|
: [
|
|
{
|
|
id: 1,
|
|
name: "김철수",
|
|
email: "kim@example.com",
|
|
phone: "010-1234-5678",
|
|
department: "개발팀",
|
|
position: "시니어 개발자",
|
|
description: "풀스택 개발자로 React, Node.js 전문가입니다. 5년 이상의 경험을 보유하고 있습니다.",
|
|
avatar: "/images/avatar1.jpg",
|
|
},
|
|
{
|
|
id: 2,
|
|
name: "이영희",
|
|
email: "lee@example.com",
|
|
phone: "010-2345-6789",
|
|
department: "디자인팀",
|
|
position: "UI/UX 디자이너",
|
|
description: "사용자 경험을 중시하는 디자이너로 Figma, Adobe XD를 능숙하게 다룹니다.",
|
|
avatar: "/images/avatar2.jpg",
|
|
},
|
|
{
|
|
id: 3,
|
|
name: "박민수",
|
|
email: "park@example.com",
|
|
phone: "010-3456-7890",
|
|
department: "기획팀",
|
|
position: "프로덕트 매니저",
|
|
description: "데이터 기반 의사결정을 통해 제품을 성장시키는 PM입니다.",
|
|
avatar: "/images/avatar3.jpg",
|
|
},
|
|
];
|
|
|
|
// 컨테이너 스타일
|
|
const containerStyle: React.CSSProperties = {
|
|
display: "grid",
|
|
gridTemplateColumns: `repeat(${cardConfig.cardsPerRow || 3}, 1fr)`,
|
|
gap: `${cardConfig.cardSpacing || 16}px`,
|
|
padding: "16px",
|
|
width: "100%",
|
|
height: "100%",
|
|
background: "transparent",
|
|
overflow: "auto",
|
|
};
|
|
|
|
// 카드 스타일
|
|
const cardStyle: React.CSSProperties = {
|
|
backgroundColor: "white",
|
|
border: "1px solid #e5e7eb",
|
|
borderRadius: "8px",
|
|
padding: "16px",
|
|
boxShadow: "0 1px 3px 0 rgba(0, 0, 0, 0.1)",
|
|
transition: "all 0.2s ease-in-out",
|
|
overflow: "hidden",
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
position: "relative",
|
|
minHeight: "200px",
|
|
cursor: isDesignMode ? "pointer" : "default",
|
|
};
|
|
|
|
// 텍스트 자르기 함수
|
|
const truncateText = (text: string, maxLength: number) => {
|
|
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 (!tableColumns || tableColumns.length === 0) return columnName;
|
|
|
|
const column = tableColumns.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 "";
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<style jsx>{`
|
|
.force-card-layout {
|
|
height: ${layout.size?.height ? `${layout.size.height}px` : "100%"} !important;
|
|
min-height: ${layout.size?.height ? `${layout.size.height}px` : "200px"} !important;
|
|
width: 100% !important;
|
|
}
|
|
.card-hover:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 0.15) !important;
|
|
border-color: #3b82f6 !important;
|
|
}
|
|
`}</style>
|
|
<div style={containerStyle} className={`card-layout force-card-layout ${className || ""}`} onClick={onClick}>
|
|
{isDesignMode
|
|
? // 디자인 모드: 존 기반 렌더링
|
|
layout.zones?.map((zone, index) => {
|
|
const zoneChildren = children?.filter((child) => child.props.parentId === zone.id) || [];
|
|
|
|
return (
|
|
<div
|
|
key={zone.id}
|
|
style={cardStyle}
|
|
onClick={() => onSelectComponent?.(zone.id)}
|
|
className="hover:border-blue-500 hover:shadow-md"
|
|
>
|
|
{/* 존 라벨 */}
|
|
<div className="absolute top-2 left-2 z-10">
|
|
<div className="rounded bg-blue-500 px-2 py-1 text-xs text-white">{zone.name}</div>
|
|
</div>
|
|
|
|
{/* 존 내용 */}
|
|
<div className="flex flex-1 flex-col pt-6">
|
|
{zoneChildren.length > 0 ? (
|
|
<div className="flex-1">{zoneChildren}</div>
|
|
) : (
|
|
<div className="flex flex-1 items-center justify-center rounded border-2 border-dashed border-gray-200 text-gray-400">
|
|
<div className="text-center">
|
|
<div className="text-sm font-medium">카드 존</div>
|
|
<div className="mt-1 text-xs">컴포넌트를 드래그하여 추가하세요</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
})
|
|
: // 실행 모드: 데이터 기반 카드 렌더링
|
|
displayData.map((item, index) => (
|
|
<div
|
|
key={item.objid || item.id || item.company_code || `card-${index}`}
|
|
style={cardStyle}
|
|
className="card-hover"
|
|
>
|
|
{/* 카드 이미지 */}
|
|
{cardConfig.cardStyle?.showImage && cardConfig.columnMapping?.imageColumn && (
|
|
<div className="mb-3 flex justify-center">
|
|
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-gray-200">
|
|
<span className="text-xl text-gray-500">👤</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 카드 타이틀 */}
|
|
{cardConfig.cardStyle?.showTitle && (
|
|
<div className="mb-2">
|
|
<h3 className="text-lg font-semibold text-gray-900">
|
|
{getColumnValue(item, cardConfig.columnMapping?.titleColumn) ||
|
|
getAutoFallbackValue(item, "title")}
|
|
</h3>
|
|
</div>
|
|
)}
|
|
|
|
{/* 카드 서브타이틀 */}
|
|
{cardConfig.cardStyle?.showSubtitle && (
|
|
<div className="mb-2">
|
|
<p className="text-sm font-medium text-blue-600">
|
|
{getColumnValue(item, cardConfig.columnMapping?.subtitleColumn) ||
|
|
getAutoFallbackValue(item, "subtitle")}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* 카드 설명 */}
|
|
{cardConfig.cardStyle?.showDescription && (
|
|
<div className="mb-3 flex-1">
|
|
<p className="text-sm leading-relaxed text-gray-600">
|
|
{truncateText(
|
|
getColumnValue(item, cardConfig.columnMapping?.descriptionColumn) ||
|
|
getAutoFallbackValue(item, "description"),
|
|
cardConfig.cardStyle?.maxDescriptionLength || 100,
|
|
)}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* 추가 표시 컬럼들 */}
|
|
{cardConfig.columnMapping?.displayColumns && cardConfig.columnMapping.displayColumns.length > 0 && (
|
|
<div className="space-y-1 border-t border-gray-100 pt-3">
|
|
{cardConfig.columnMapping.displayColumns.map((columnName, idx) => {
|
|
const value = getColumnValue(item, columnName);
|
|
if (!value) return null;
|
|
|
|
return (
|
|
<div key={idx} className="flex justify-between text-xs">
|
|
<span className="text-gray-500 capitalize">{getColumnLabel(columnName)}:</span>
|
|
<span className="font-medium text-gray-700">{value}</span>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
|
|
{/* 카드 액션 (선택사항) */}
|
|
<div className="mt-3 flex justify-end space-x-2">
|
|
<button className="text-xs font-medium text-blue-600 hover:text-blue-800">상세보기</button>
|
|
<button className="text-xs font-medium text-gray-500 hover:text-gray-700">편집</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{/* 빈 상태 표시 */}
|
|
{!isDesignMode && displayData.length === 0 && (
|
|
<div className="col-span-full flex items-center justify-center p-8">
|
|
<div className="text-center text-gray-500">
|
|
<div className="mb-2 text-lg">📋</div>
|
|
<div className="text-sm">표시할 데이터가 없습니다</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</>
|
|
);
|
|
};
|