270 lines
8.4 KiB
TypeScript
270 lines
8.4 KiB
TypeScript
|
|
"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>
|
||
|
|
);
|
||
|
|
};
|