ERP-node/frontend/lib/registry/pop-components/pop-dashboard/modes/GridMode.tsx

178 lines
4.9 KiB
TypeScript
Raw Normal View History

"use client";
/**
*
*
* CSS Grid로 ( / )
* @container
*
* :
* - containerWidth에
* - ,
* - (MIN_CELL_WIDTH)
*/
import React, { useMemo } from "react";
import type { DashboardCell } from "../../types";
// ===== 상수 =====
/** 셀 하나의 최소 너비 (px). 이보다 좁아지면 열 수 축소 */
const MIN_CELL_WIDTH = 80;
// ===== Props =====
export interface GridModeProps {
/** 셀 배치 정보 */
cells: DashboardCell[];
/** 설정된 열 수 (최대값) */
columns: number;
/** 설정된 행 수 */
rows: number;
/** 아이템 간 간격 (px) */
gap?: number;
/** 컨테이너 너비 (px, 반응형 자동 조정용) */
containerWidth?: number;
/** 셀의 아이템 렌더링. itemId가 null이면 빈 셀 */
renderItem: (itemId: string) => React.ReactNode;
}
// ===== 반응형 열 수 계산 =====
/**
*
*
* columns가 , .
* gap도 .
*
* : columns=3, containerWidth=400, gap=8, MIN_CELL_WIDTH=160
* = 400 - (3-1)*8 = 384
* = 384/3 = 128 < 160 ->
* columns=2: 사용 = 400 - (2-1)*8 = 392, = 196 >= 160 -> OK
*/
function computeResponsiveColumns(
configColumns: number,
containerWidth: number,
gap: number
): number {
if (containerWidth <= 0) return configColumns;
for (let cols = configColumns; cols >= 1; cols--) {
const totalGap = (cols - 1) * gap;
const cellWidth = (containerWidth - totalGap) / cols;
if (cellWidth >= MIN_CELL_WIDTH) return cols;
}
return 1;
}
/**
*
*
* gridColumn/gridRow를 actualColumns에
* ,
*/
function remapCells(
cells: DashboardCell[],
configColumns: number,
actualColumns: number,
configRows: number
): { remappedCells: DashboardCell[]; actualRows: number } {
// 열 수가 같으면 원본 그대로
if (actualColumns >= configColumns) {
return { remappedCells: cells, actualRows: configRows };
}
// 셀을 원래 위치 순서대로 정렬 (행 우선)
const sorted = [...cells].sort((a, b) => {
const aRow = parseInt(a.gridRow.split(" / ")[0]) || 0;
const bRow = parseInt(b.gridRow.split(" / ")[0]) || 0;
if (aRow !== bRow) return aRow - bRow;
const aCol = parseInt(a.gridColumn.split(" / ")[0]) || 0;
const bCol = parseInt(b.gridColumn.split(" / ")[0]) || 0;
return aCol - bCol;
});
// 순서대로 새 위치에 배치
let maxRow = 0;
const remapped = sorted.map((cell, index) => {
const newCol = (index % actualColumns) + 1;
const newRow = Math.floor(index / actualColumns) + 1;
maxRow = Math.max(maxRow, newRow);
return {
...cell,
gridColumn: `${newCol} / ${newCol + 1}`,
gridRow: `${newRow} / ${newRow + 1}`,
};
});
return { remappedCells: remapped, actualRows: maxRow };
}
// ===== 메인 컴포넌트 =====
export function GridModeComponent({
cells,
columns,
rows,
gap = 8,
containerWidth,
renderItem,
}: GridModeProps) {
// 반응형 열 수 계산
const actualColumns = useMemo(
() =>
containerWidth
? computeResponsiveColumns(columns, containerWidth, gap)
: columns,
[columns, containerWidth, gap]
);
// 열 수가 줄었으면 셀 재배열
const { remappedCells, actualRows } = useMemo(
() => remapCells(cells, columns, actualColumns, rows),
[cells, columns, actualColumns, rows]
);
if (!remappedCells.length) {
return (
<div className="flex h-full w-full items-center justify-center">
<span className="text-xs text-muted-foreground"> </span>
</div>
);
}
return (
<div
className="h-full w-full"
style={{
display: "grid",
gridTemplateColumns: `repeat(${actualColumns}, 1fr)`,
gridTemplateRows: `repeat(${actualRows}, 1fr)`,
gap: `${gap}px`,
}}
>
{remappedCells.map((cell) => (
<div
key={cell.id}
className="@container min-h-0 min-w-0 overflow-hidden rounded-md border border-border/50 bg-card"
style={{
gridColumn: cell.gridColumn,
gridRow: cell.gridRow,
}}
>
{cell.itemId ? (
renderItem(cell.itemId)
) : (
<div className="flex h-full w-full items-center justify-center">
<span className="text-[10px] text-muted-foreground/50">
</span>
</div>
)}
</div>
))}
</div>
);
}