ERP-node/frontend/lib/registry/layouts/grid/GridLayout.tsx

282 lines
9.9 KiB
TypeScript

"use client";
import React from "react";
import { LayoutRendererProps } from "../BaseLayoutRenderer";
import { filterDOMProps } from "@/lib/utils/domPropsFilter";
/**
* 그리드 레이아웃 컴포넌트
*/
export interface GridLayoutProps extends LayoutRendererProps {
renderer: any; // GridLayoutRenderer 타입
}
export const GridLayout: React.FC<GridLayoutProps> = ({
layout,
isDesignMode = false,
isSelected = false,
onClick,
className = "",
renderer,
onZoneComponentDrop,
onZoneClick,
...props
}) => {
if (!layout.layoutConfig.grid) {
return (
<div className="error-layout flex items-center justify-center rounded border-2 border-red-300 bg-red-50 p-4">
<div className="text-center text-red-600">
<div className="font-medium"> .</div>
<div className="mt-1 text-sm">layoutConfig.grid가 .</div>
</div>
</div>
);
}
const gridConfig = layout.layoutConfig.grid;
const containerStyle = renderer.getLayoutContainerStyle();
// 그리드 컨테이너 스타일
const gridStyle: React.CSSProperties = {
display: "grid",
gridTemplateRows: `repeat(${gridConfig.rows}, 1fr)`,
gridTemplateColumns: `repeat(${gridConfig.columns}, 1fr)`,
gap: `${gridConfig.gap || 16}px`,
// 레이아웃 컴포넌트의 높이 적용 - 부모 높이를 100% 따라가도록
height: layout.size?.height ? `${layout.size.height}px` : "100%",
width: layout.size?.width ? `${layout.size.width}px` : "100%",
minHeight: layout.size?.height ? `${layout.size.height}px` : "200px", // 최소 높이 보장
// containerStyle을 나중에 적용하되, grid 관련 속성은 덮어쓰지 않도록
...containerStyle,
// grid 속성들을 다시 강제 적용
display: "grid",
gridTemplateRows: `repeat(${gridConfig.rows}, 1fr)`,
gridTemplateColumns: `repeat(${gridConfig.columns}, 1fr)`,
height: layout.size?.height ? `${layout.size.height}px` : "100%",
minHeight: layout.size?.height ? `${layout.size.height}px` : "200px",
};
// 디자인 모드 스타일
if (isDesignMode) {
gridStyle.border = isSelected ? "2px solid #3b82f6" : "1px solid #e2e8f0";
gridStyle.borderRadius = "8px";
}
// DOM 안전한 props만 필터링
const domProps = filterDOMProps(props);
return (
<>
<style jsx>{`
.force-grid-layout {
display: grid !important;
height: ${layout.size?.height ? `${layout.size.height}px` : "100%"} !important;
min-height: ${layout.size?.height ? `${layout.size.height}px` : "200px"} !important;
width: 100% !important;
}
`}</style>
<div
className={`grid-layout force-grid-layout ${isDesignMode ? "design-mode" : ""} ${className}`}
style={gridStyle}
onClick={onClick}
draggable={isDesignMode}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
{...domProps}
>
{layout.zones.map((zone: any) => {
const zoneChildren = renderer.getZoneChildren(zone.id);
// 레이아웃 전체 높이를 행 수로 나누어 각 존의 기본 높이 계산
// layout은 LayoutComponent이고, 실제 높이는 layout.size.height에 있음
const layoutHeight = layout.size?.height || 400; // 기본값 400px
const rowHeight = Math.floor(layoutHeight / gridConfig.rows);
console.log("🔍 GridLayout 높이 정보:", {
layoutId: layout.id,
layoutSize: layout.size,
layoutHeight,
rowHeight,
gridRows: gridConfig.rows,
zoneId: zone.id,
zoneSize: zone.size,
});
// 그리드 위치 설정
const zoneStyle: React.CSSProperties = {
gridRow: zone.position.row !== undefined ? zone.position.row + 1 : undefined,
gridColumn: zone.position.column !== undefined ? zone.position.column + 1 : undefined,
// 카드 레이아웃처럼 항상 명확한 경계 표시
backgroundColor: "white",
border: "1px solid #e5e7eb",
borderRadius: "8px",
padding: "16px",
boxShadow: "0 1px 3px 0 rgba(0, 0, 0, 0.1)",
// 존의 높이: 개별 설정이 있으면 우선, 없으면 부모 높이를 100% 따라가도록
height: zone.size?.height
? typeof zone.size.height === "number"
? `${zone.size.height}px`
: zone.size.height
: "100%", // 그리드 셀의 높이를 100% 따라감
minHeight: zone.size?.minHeight
? typeof zone.size.minHeight === "number"
? `${zone.size.minHeight}px`
: zone.size.minHeight
: "100px",
maxHeight: zone.size?.maxHeight
? typeof zone.size.maxHeight === "number"
? `${zone.size.maxHeight}px`
: zone.size.maxHeight
: "none",
position: "relative",
transition: "all 0.2s ease",
overflow: "hidden",
display: "flex",
flexDirection: "column",
};
return (
<div
key={zone.id}
style={zoneStyle}
className="grid-zone"
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = "#3b82f6";
e.currentTarget.style.boxShadow =
"0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 0 0 2px rgba(59, 130, 246, 0.1)";
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = "#e5e7eb";
e.currentTarget.style.boxShadow = "0 1px 3px 0 rgba(0, 0, 0, 0.1)";
}}
// 드롭 이벤트 제거 - 일반 캔버스 드롭만 사용
onClick={(e) => {
e.stopPropagation();
if (onZoneClick) {
onZoneClick(zone.id);
}
}}
>
{/* 존 라벨 */}
{isDesignMode && (
<div
className="zone-label"
style={{
position: "absolute",
top: "-2px",
left: "8px",
backgroundColor: "#3b82f6",
color: "white",
fontSize: "10px",
padding: "2px 6px",
borderRadius: "0 0 4px 4px",
fontWeight: "500",
zIndex: 10,
}}
>
{zone.name || zone.id}
</div>
)}
{/* 존 내용 */}
<div
style={{
paddingTop: isDesignMode ? "16px" : "0",
flex: 1,
position: "relative",
minHeight: "100px",
}}
className="grid-zone-content"
>
{/* 존 안의 컴포넌트들을 절대 위치로 렌더링 */}
{zoneChildren.map((child: any) => (
<div
key={child.id}
style={{
position: "absolute",
left: child.position?.x || 0,
top: child.position?.y || 0,
width: child.size?.width || "auto",
height: child.size?.height || "auto",
zIndex: child.position?.z || 1,
}}
>
{renderer.renderChild(child)}
</div>
))}
</div>
</div>
);
})}
{/* 디자인 모드에서 빈 그리드 셀 표시 */}
{isDesignMode && <GridEmptyCells gridConfig={gridConfig} layout={layout} isDesignMode={isDesignMode} />}
</div>
</>
);
};
/**
* 빈 그리드 셀들을 렌더링하는 컴포넌트
*/
const GridEmptyCells: React.FC<{
gridConfig: any;
layout: any;
isDesignMode: boolean;
}> = ({ gridConfig, layout, isDesignMode }) => {
const totalCells = gridConfig.rows * gridConfig.columns;
const occupiedCells = new Set(
layout.zones
.map((zone: any) =>
zone.position.row !== undefined && zone.position.column !== undefined
? zone.position.row * gridConfig.columns + zone.position.column
: -1,
)
.filter((index: number) => index >= 0),
);
const emptyCells: React.ReactElement[] = [];
for (let i = 0; i < totalCells; i++) {
if (!occupiedCells.has(i)) {
const row = Math.floor(i / gridConfig.columns);
const column = i % gridConfig.columns;
emptyCells.push(
<div
key={`empty-${i}`}
className="empty-grid-cell"
style={{
gridRow: row + 1,
gridColumn: column + 1,
border: isDesignMode ? "1px dashed #cbd5e1" : "1px solid #f1f5f9",
borderRadius: "4px",
backgroundColor: isDesignMode ? "rgba(148, 163, 184, 0.05)" : "rgba(248, 250, 252, 0.3)",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "10px",
color: "#94a3b8",
minHeight: "40px",
transition: "all 0.2s ease",
}}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = "rgba(59, 130, 246, 0.05)";
e.currentTarget.style.borderColor = "#3b82f6";
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = isDesignMode
? "rgba(148, 163, 184, 0.05)"
: "rgba(248, 250, 252, 0.3)";
e.currentTarget.style.borderColor = isDesignMode ? "#cbd5e1" : "#f1f5f9";
}}
>
{isDesignMode ? `${row + 1},${column + 1}` : ""}
</div>,
);
}
}
return <>{emptyCells}</>;
};