ERP-node/frontend/lib/registry/layouts/card-layout/CardLayoutLayout.tsx

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>
</>
);
};