179 lines
4.6 KiB
TypeScript
179 lines
4.6 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* Layout 메타 컴포넌트 렌더러
|
|
* - config.mode에 따라 children 배치:
|
|
* - columns: CSS Grid (grid-cols-{n})
|
|
* - rows: flex flex-col
|
|
* - tabs: shadcn Tabs 사용
|
|
* - card: shadcn Card 사용
|
|
* - children은 props.children으로 받음 (React.ReactNode)
|
|
* - config.gap으로 간격 설정
|
|
* - 디자인 모드: 점선 border + 레이아웃 모드 라벨 표시
|
|
*/
|
|
|
|
import React from "react";
|
|
import { cn } from "@/lib/utils";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
|
|
interface LayoutArea {
|
|
id: string;
|
|
label?: string;
|
|
size?: string; // "1fr", "2fr", "auto", "200px"
|
|
}
|
|
|
|
interface LayoutTab {
|
|
id: string;
|
|
label: string;
|
|
}
|
|
|
|
interface LayoutRendererProps {
|
|
id: string;
|
|
config: {
|
|
mode: "columns" | "rows" | "tabs" | "card";
|
|
areas?: LayoutArea[];
|
|
tabs?: LayoutTab[];
|
|
gap?: number;
|
|
title?: string;
|
|
};
|
|
children?: React.ReactNode; // 자식 컴포넌트들
|
|
isDesignMode?: boolean;
|
|
className?: string;
|
|
}
|
|
|
|
export function LayoutRenderer({
|
|
id,
|
|
config,
|
|
children,
|
|
isDesignMode = false,
|
|
className,
|
|
}: LayoutRendererProps) {
|
|
const { mode, areas = [], tabs = [], gap = 4, title } = config;
|
|
|
|
// columns 모드: CSS Grid 가로 분할
|
|
if (mode === "columns") {
|
|
const gridCols = areas.length || 2;
|
|
const gridTemplateColumns = areas
|
|
.map((a) => a.size || "1fr")
|
|
.join(" ");
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"grid w-full",
|
|
isDesignMode && "rounded-md border-2 border-dashed border-primary/30",
|
|
className
|
|
)}
|
|
style={{
|
|
gridTemplateColumns,
|
|
gap: `${gap * 4}px`,
|
|
}}
|
|
>
|
|
{isDesignMode && areas.length === 0 ? (
|
|
<div className="col-span-full flex h-20 items-center justify-center text-sm text-muted-foreground">
|
|
Columns 레이아웃 (영역 추가 필요)
|
|
</div>
|
|
) : (
|
|
children
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// rows 모드: flex 세로 분할
|
|
if (mode === "rows") {
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"flex w-full flex-col",
|
|
isDesignMode && "rounded-md border-2 border-dashed border-primary/30",
|
|
className
|
|
)}
|
|
style={{ gap: `${gap * 4}px` }}
|
|
>
|
|
{isDesignMode && areas.length === 0 ? (
|
|
<div className="flex h-20 items-center justify-center text-sm text-muted-foreground">
|
|
Rows 레이아웃 (영역 추가 필요)
|
|
</div>
|
|
) : (
|
|
children
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// tabs 모드: shadcn Tabs
|
|
if (mode === "tabs") {
|
|
if (tabs.length === 0) {
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"flex h-20 items-center justify-center rounded-md border-2 border-dashed border-primary/30 text-sm text-muted-foreground",
|
|
className
|
|
)}
|
|
>
|
|
Tabs 레이아웃 (탭 추가 필요)
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className={cn("w-full", className)}>
|
|
{title && <h2 className="mb-2 text-lg font-semibold">{title}</h2>}
|
|
<Tabs defaultValue={tabs[0]?.id}>
|
|
<TabsList>
|
|
{tabs.map((tab) => (
|
|
<TabsTrigger key={tab.id} value={tab.id}>
|
|
{tab.label}
|
|
</TabsTrigger>
|
|
))}
|
|
</TabsList>
|
|
{tabs.map((tab) => (
|
|
<TabsContent key={tab.id} value={tab.id} className="mt-4">
|
|
{isDesignMode ? (
|
|
<div className="flex h-20 items-center justify-center rounded-md border border-dashed text-sm text-muted-foreground">
|
|
{tab.label} 탭 내용
|
|
</div>
|
|
) : (
|
|
children
|
|
)}
|
|
</TabsContent>
|
|
))}
|
|
</Tabs>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// card 모드: shadcn Card
|
|
if (mode === "card") {
|
|
return (
|
|
<Card className={cn("w-full", className)}>
|
|
{title && (
|
|
<CardHeader>
|
|
<CardTitle>{title}</CardTitle>
|
|
</CardHeader>
|
|
)}
|
|
<CardContent
|
|
className="space-y-4"
|
|
style={{ gap: `${gap * 4}px` }}
|
|
>
|
|
{isDesignMode && !children ? (
|
|
<div className="flex h-20 items-center justify-center text-sm text-muted-foreground">
|
|
Card 레이아웃 (내용 추가 필요)
|
|
</div>
|
|
) : (
|
|
children
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="text-destructive">
|
|
Unknown layout mode: {mode}
|
|
</div>
|
|
);
|
|
}
|