ERP-node/frontend/components/screen/GridLayoutBuilder.tsx

270 lines
8.4 KiB
TypeScript
Raw Normal View History

2025-10-13 18:28:03 +09:00
"use client";
import React, { useState, useCallback } from "react";
import { cn } from "@/lib/utils";
import { GridLayout, LayoutRow, RowComponent, CreateRowOptions } from "@/types/grid-system";
import { ComponentData } from "@/types/screen";
import { LayoutRowRenderer } from "./LayoutRowRenderer";
import { Button } from "@/components/ui/button";
import { Plus, Grid3x3 } from "lucide-react";
import { GAP_PRESETS } from "@/lib/constants/columnSpans";
interface GridLayoutBuilderProps {
layout: GridLayout;
onUpdateLayout: (layout: GridLayout) => void;
selectedRowId?: string;
selectedComponentId?: string;
onSelectRow?: (rowId: string) => void;
onSelectComponent?: (componentId: string) => void;
showGridGuides?: boolean;
}
export const GridLayoutBuilder: React.FC<GridLayoutBuilderProps> = ({
layout,
onUpdateLayout,
selectedRowId,
selectedComponentId,
onSelectRow,
onSelectComponent,
showGridGuides = true,
}) => {
const [isDraggingOver, setIsDraggingOver] = useState(false);
// 새 행 추가
const addNewRow = useCallback(
(options?: CreateRowOptions) => {
const newRow: LayoutRow = {
id: `row-${Date.now()}`,
rowIndex: layout.rows.length,
height: options?.height || "auto",
fixedHeight: options?.fixedHeight,
gap: options?.gap || "sm",
padding: options?.padding || "sm",
alignment: options?.alignment || "start",
verticalAlignment: "middle",
components: [],
};
onUpdateLayout({
...layout,
rows: [...layout.rows, newRow],
});
// 새로 추가된 행 선택
if (onSelectRow) {
onSelectRow(newRow.id);
}
},
[layout, onUpdateLayout, onSelectRow],
);
// 행 삭제
const deleteRow = useCallback(
(rowId: string) => {
const updatedRows = layout.rows
.filter((row) => row.id !== rowId)
.map((row, index) => ({
...row,
rowIndex: index,
}));
onUpdateLayout({
...layout,
rows: updatedRows,
});
},
[layout, onUpdateLayout],
);
// 행 순서 변경
const moveRow = useCallback(
(rowId: string, direction: "up" | "down") => {
const rowIndex = layout.rows.findIndex((row) => row.id === rowId);
if (rowIndex === -1) return;
const newIndex = direction === "up" ? rowIndex - 1 : rowIndex + 1;
if (newIndex < 0 || newIndex >= layout.rows.length) return;
const updatedRows = [...layout.rows];
[updatedRows[rowIndex], updatedRows[newIndex]] = [updatedRows[newIndex], updatedRows[rowIndex]];
// 인덱스 재정렬
updatedRows.forEach((row, index) => {
row.rowIndex = index;
});
onUpdateLayout({
...layout,
rows: updatedRows,
});
},
[layout, onUpdateLayout],
);
// 행 업데이트
const updateRow = useCallback(
(rowId: string, updates: Partial<LayoutRow>) => {
const updatedRows = layout.rows.map((row) => (row.id === rowId ? { ...row, ...updates } : row));
onUpdateLayout({
...layout,
rows: updatedRows,
});
},
[layout, onUpdateLayout],
);
// 컴포넌트 선택
const handleSelectComponent = useCallback(
(componentId: string) => {
if (onSelectComponent) {
onSelectComponent(componentId);
}
},
[onSelectComponent],
);
// 행 선택
const handleSelectRow = useCallback(
(rowId: string) => {
if (onSelectRow) {
onSelectRow(rowId);
}
},
[onSelectRow],
);
// 컨테이너 클래스
const containerClasses = cn("w-full h-full overflow-auto bg-gray-50 relative", isDraggingOver && "bg-blue-50");
// 글로벌 컨테이너 클래스
const globalContainerClasses = cn(
"mx-auto relative",
layout.globalSettings.containerMaxWidth === "full" ? "w-full" : `max-w-${layout.globalSettings.containerMaxWidth}`,
GAP_PRESETS[layout.globalSettings.containerPadding].class.replace("gap-", "px-"),
);
return (
<div className={containerClasses}>
{/* 그리드 가이드라인 */}
{showGridGuides && (
<div className="pointer-events-none absolute inset-0 z-0">
<div className={globalContainerClasses}>
<div className="grid h-full grid-cols-12">
{Array.from({ length: 12 }).map((_, i) => (
<div key={i} className="h-full border-l border-dashed border-gray-300 opacity-30" />
))}
</div>
</div>
</div>
)}
{/* 메인 컨테이너 */}
<div className={cn(globalContainerClasses, "relative z-10 py-8")}>
{layout.rows.length === 0 ? (
// 빈 레이아웃
<div className="flex min-h-[400px] flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-white">
<Grid3x3 className="mb-4 h-16 w-16 text-gray-300" />
<h3 className="mb-2 text-lg font-medium text-gray-600"> </h3>
<p className="mb-6 text-sm text-gray-500"> </p>
<Button onClick={() => addNewRow()} size="lg">
<Plus className="mr-2 h-5 w-5" />
</Button>
</div>
) : (
// 행 목록
<div className="space-y-4">
{layout.rows.map((row) => (
<LayoutRowRenderer
key={row.id}
row={row}
components={layout.components}
isSelected={selectedRowId === row.id}
selectedComponentId={selectedComponentId}
onSelectRow={() => handleSelectRow(row.id)}
onSelectComponent={handleSelectComponent}
onUpdateRow={(updatedRow) => updateRow(row.id, updatedRow)}
/>
))}
</div>
)}
{/* 새 행 추가 버튼 */}
{layout.rows.length > 0 && (
<div className="mt-6 flex justify-center">
<div className="inline-flex flex-col gap-2">
<Button onClick={() => addNewRow()} variant="outline" size="lg" className="w-full">
<Plus className="mr-2 h-5 w-5" />
</Button>
{/* 빠른 추가 버튼들 */}
<div className="flex gap-2">
<Button
onClick={() =>
addNewRow({
gap: "sm",
padding: "sm",
alignment: "start",
})
}
variant="ghost"
size="sm"
className="flex-1"
>
</Button>
<Button
onClick={() =>
addNewRow({
gap: "md",
padding: "md",
alignment: "stretch",
})
}
variant="ghost"
size="sm"
className="flex-1"
>
2
</Button>
<Button
onClick={() =>
addNewRow({
gap: "none",
padding: "md",
alignment: "stretch",
})
}
variant="ghost"
size="sm"
className="flex-1"
>
</Button>
</div>
</div>
</div>
)}
{/* 레이아웃 정보 */}
<div className="mt-8 rounded-lg border bg-white p-4">
<div className="flex items-center justify-between text-sm text-gray-600">
<div className="flex items-center gap-4">
<span>: {layout.rows.length}</span>
<span>: {layout.components.size}</span>
<span>
:{" "}
{layout.globalSettings.containerMaxWidth === "full" ? "전체" : layout.globalSettings.containerMaxWidth}
</span>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-gray-400">12 </span>
{showGridGuides && <span className="text-xs text-green-600"> </span>}
</div>
</div>
</div>
</div>
</div>
);
};