ERP-node/frontend/components/screen/panels/LayoutsPanel.tsx

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-muted-foreground" />
<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-muted-foreground">{layout.description}</p>
)}
<div className="mt-2 text-xs text-gray-500"> : {layout.defaultZones.length}</div>
</CardContent>
</Card>
);
})}
</div>
)}
</div>
</Tabs>
</div>
</div>
);
}