236 lines
8.5 KiB
TypeScript
236 lines
8.5 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useMemo } from "react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import { Grid, Layout, LayoutDashboard, Table, Navigation, FileText, Building, Search, Plus } from "lucide-react";
|
|
import { LAYOUT_CATEGORIES, LayoutCategory } from "@/types/layout";
|
|
import { LayoutRegistry } from "@/lib/registry/LayoutRegistry";
|
|
import { calculateGridInfo, calculateWidthFromColumns } from "@/lib/utils/gridUtils";
|
|
|
|
// 카테고리 아이콘 매핑
|
|
const CATEGORY_ICONS = {
|
|
basic: Grid,
|
|
form: FileText,
|
|
table: Table,
|
|
dashboard: LayoutDashboard,
|
|
navigation: Navigation,
|
|
content: Layout,
|
|
business: Building,
|
|
};
|
|
|
|
// 카테고리 이름 매핑
|
|
const CATEGORY_NAMES = {
|
|
basic: "기본",
|
|
form: "폼",
|
|
table: "테이블",
|
|
dashboard: "대시보드",
|
|
navigation: "네비게이션",
|
|
content: "컨텐츠",
|
|
business: "업무용",
|
|
};
|
|
|
|
interface LayoutsPanelProps {
|
|
onDragStart: (e: React.DragEvent, layoutData: any) => void;
|
|
onLayoutSelect?: (layoutDefinition: any) => void;
|
|
className?: string;
|
|
gridSettings?: {
|
|
columns: number;
|
|
gap: number;
|
|
padding: number;
|
|
snapToGrid: boolean;
|
|
};
|
|
screenResolution?: {
|
|
width: number;
|
|
height: number;
|
|
};
|
|
}
|
|
|
|
export default function LayoutsPanel({
|
|
onDragStart,
|
|
onLayoutSelect,
|
|
className,
|
|
gridSettings,
|
|
screenResolution,
|
|
}: LayoutsPanelProps) {
|
|
const [searchTerm, setSearchTerm] = useState("");
|
|
const [selectedCategory, setSelectedCategory] = useState<string>("all");
|
|
|
|
// 레지스트리에서 레이아웃 조회
|
|
const allLayouts = useMemo(() => LayoutRegistry.getAllLayouts(), []);
|
|
|
|
// 필터링된 레이아웃
|
|
const filteredLayouts = useMemo(() => {
|
|
let layouts = allLayouts;
|
|
|
|
// 카테고리 필터
|
|
if (selectedCategory !== "all") {
|
|
layouts = layouts.filter((layout) => layout.category === selectedCategory);
|
|
}
|
|
|
|
// 검색 필터
|
|
if (searchTerm) {
|
|
layouts = layouts.filter(
|
|
(layout) =>
|
|
layout.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
layout.nameEng?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
layout.description?.toLowerCase().includes(searchTerm.toLowerCase()),
|
|
);
|
|
}
|
|
|
|
return layouts;
|
|
}, [allLayouts, selectedCategory, searchTerm]);
|
|
|
|
// 카테고리별 개수
|
|
const categoryCounts = useMemo(() => {
|
|
const counts: Record<string, number> = {};
|
|
Object.values(LAYOUT_CATEGORIES).forEach((category) => {
|
|
counts[category] = allLayouts.filter((layout) => layout.category === category).length;
|
|
});
|
|
return counts;
|
|
}, [allLayouts]);
|
|
|
|
// 레이아웃 드래그 시작 핸들러
|
|
const handleDragStart = (e: React.DragEvent, layoutDefinition: any) => {
|
|
// 격자 기반 동적 크기 계산
|
|
let calculatedSize = layoutDefinition.defaultSize || { width: 400, height: 300 };
|
|
|
|
if (gridSettings && screenResolution && layoutDefinition.id === "card-layout") {
|
|
// 카드 레이아웃의 경우 8그리드 컬럼에 맞는 너비 계산
|
|
const gridInfo = calculateGridInfo(screenResolution.width, screenResolution.height, gridSettings);
|
|
const calculatedWidth = calculateWidthFromColumns(8, gridInfo, gridSettings);
|
|
|
|
calculatedSize = {
|
|
width: Math.max(calculatedWidth, 400), // 최소 400px 보장
|
|
height: 400, // 높이는 고정
|
|
};
|
|
|
|
// console.log("🎯 카드 레이아웃 동적 크기 계산:", {
|
|
// gridColumns: 8,
|
|
// screenResolution,
|
|
// gridSettings,
|
|
// gridInfo,
|
|
// calculatedWidth,
|
|
// finalSize: calculatedSize,
|
|
// });
|
|
}
|
|
|
|
// 새 레이아웃 컴포넌트 데이터 생성
|
|
const layoutData = {
|
|
id: `layout_${Date.now()}`,
|
|
type: "layout",
|
|
layoutType: layoutDefinition.id,
|
|
layoutConfig: layoutDefinition.defaultConfig,
|
|
zones: layoutDefinition.defaultZones,
|
|
children: [],
|
|
allowedComponentTypes: [],
|
|
position: { x: 0, y: 0 },
|
|
size: calculatedSize,
|
|
label: layoutDefinition.name,
|
|
gridColumns: layoutDefinition.id === "card-layout" ? 8 : 1, // 카드 레이아웃은 기본 8그리드
|
|
};
|
|
|
|
// 드래그 데이터 설정
|
|
e.dataTransfer.setData("application/json", JSON.stringify(layoutData));
|
|
e.dataTransfer.setData("text/plain", layoutDefinition.name);
|
|
e.dataTransfer.effectAllowed = "copy";
|
|
|
|
onDragStart(e, layoutData);
|
|
};
|
|
|
|
// 레이아웃 선택 핸들러
|
|
const handleLayoutSelect = (layoutDefinition: any) => {
|
|
onLayoutSelect?.(layoutDefinition);
|
|
};
|
|
|
|
return (
|
|
<div className={`layouts-panel h-full bg-gradient-to-br from-slate-50 to-indigo-50/30 border-r border-gray-200/60 shadow-sm ${className || ""}`}>
|
|
<div className="flex h-full flex-col">
|
|
{/* 헤더 */}
|
|
<div className="border-b p-4">
|
|
<div className="mb-3 flex items-center justify-between">
|
|
<h3 className="text-lg font-semibold">레이아웃</h3>
|
|
<Button size="sm" variant="outline">
|
|
<Plus className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
|
|
{/* 검색 */}
|
|
<div className="relative">
|
|
<Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 transform text-gray-400" />
|
|
<Input
|
|
placeholder="레이아웃 검색..."
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
className="pl-10"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 카테고리 탭 */}
|
|
<Tabs value={selectedCategory} onValueChange={setSelectedCategory} className="flex-1">
|
|
<TabsList className="grid w-full grid-cols-4 px-4 pt-2">
|
|
<TabsTrigger value="all" className="text-xs">
|
|
전체 ({allLayouts.length})
|
|
</TabsTrigger>
|
|
<TabsTrigger value="basic" className="text-xs">
|
|
기본 ({categoryCounts.basic || 0})
|
|
</TabsTrigger>
|
|
<TabsTrigger value="form" className="text-xs">
|
|
폼 ({categoryCounts.form || 0})
|
|
</TabsTrigger>
|
|
<TabsTrigger value="navigation" className="text-xs">
|
|
탭 ({categoryCounts.navigation || 0})
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
|
|
{/* 레이아웃 목록 */}
|
|
<div className="flex-1 overflow-auto p-4">
|
|
{filteredLayouts.length === 0 ? (
|
|
<div className="flex h-32 items-center justify-center text-center text-sm text-gray-500">
|
|
{searchTerm ? "검색 결과가 없습니다." : "레이아웃이 없습니다."}
|
|
</div>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{filteredLayouts.map((layout) => {
|
|
const CategoryIcon = CATEGORY_ICONS[layout.category as keyof typeof CATEGORY_ICONS];
|
|
return (
|
|
<Card
|
|
key={layout.id}
|
|
className="cursor-move transition-shadow hover:shadow-md"
|
|
draggable
|
|
onDragStart={(e) => handleDragStart(e, layout)}
|
|
onClick={() => handleLayoutSelect(layout)}
|
|
>
|
|
<CardHeader className="pb-2">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<CategoryIcon className="h-4 w-4 text-gray-600" />
|
|
<Badge variant="secondary" className="text-xs">
|
|
{CATEGORY_NAMES[layout.category as keyof typeof CATEGORY_NAMES]}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
<CardTitle className="text-sm">{layout.name}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="pt-0">
|
|
{layout.description && (
|
|
<p className="line-clamp-2 text-xs text-gray-600">{layout.description}</p>
|
|
)}
|
|
<div className="mt-2 text-xs text-gray-500">존 개수: {layout.defaultZones.length}개</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Tabs>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|