컴포넌트 화면편집기에 배치
This commit is contained in:
135
frontend/lib/registry/DynamicComponentRenderer.tsx
Normal file
135
frontend/lib/registry/DynamicComponentRenderer.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { ComponentData } from "@/types/screen";
|
||||
|
||||
// 컴포넌트 렌더러 인터페이스
|
||||
export interface ComponentRenderer {
|
||||
(props: {
|
||||
component: ComponentData;
|
||||
isSelected?: boolean;
|
||||
isInteractive?: boolean;
|
||||
formData?: Record<string, any>;
|
||||
onFormDataChange?: (fieldName: string, value: any) => void;
|
||||
onClick?: (e?: React.MouseEvent) => void;
|
||||
onDragStart?: (e: React.DragEvent) => void;
|
||||
onDragEnd?: () => void;
|
||||
children?: React.ReactNode;
|
||||
[key: string]: any;
|
||||
}): React.ReactElement;
|
||||
}
|
||||
|
||||
// 컴포넌트 레지스트리
|
||||
class ComponentRegistry {
|
||||
private renderers: Map<string, ComponentRenderer> = new Map();
|
||||
|
||||
// 컴포넌트 렌더러 등록
|
||||
register(componentType: string, renderer: ComponentRenderer) {
|
||||
this.renderers.set(componentType, renderer);
|
||||
console.log(`🔧 컴포넌트 렌더러 등록: ${componentType}`);
|
||||
}
|
||||
|
||||
// 컴포넌트 렌더러 조회
|
||||
get(componentType: string): ComponentRenderer | undefined {
|
||||
return this.renderers.get(componentType);
|
||||
}
|
||||
|
||||
// 등록된 모든 컴포넌트 타입 조회
|
||||
getRegisteredTypes(): string[] {
|
||||
return Array.from(this.renderers.keys());
|
||||
}
|
||||
|
||||
// 컴포넌트 타입이 등록되어 있는지 확인
|
||||
has(componentType: string): boolean {
|
||||
const result = this.renderers.has(componentType);
|
||||
console.log(`🔍 ComponentRegistry.has("${componentType}"):`, {
|
||||
result,
|
||||
availableKeys: Array.from(this.renderers.keys()),
|
||||
mapSize: this.renderers.size,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 컴포넌트 레지스트리 인스턴스
|
||||
export const componentRegistry = new ComponentRegistry();
|
||||
|
||||
// 동적 컴포넌트 렌더러 컴포넌트
|
||||
export interface DynamicComponentRendererProps {
|
||||
component: ComponentData;
|
||||
isSelected?: boolean;
|
||||
onClick?: (e?: React.MouseEvent) => void;
|
||||
onDragStart?: (e: React.DragEvent) => void;
|
||||
onDragEnd?: () => void;
|
||||
children?: React.ReactNode;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> = ({
|
||||
component,
|
||||
isSelected = false,
|
||||
onClick,
|
||||
onDragStart,
|
||||
onDragEnd,
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
// component_config에서 실제 컴포넌트 타입 추출
|
||||
const componentType = component.componentConfig?.type || component.type;
|
||||
|
||||
console.log("🎯 DynamicComponentRenderer:", {
|
||||
componentId: component.id,
|
||||
componentType,
|
||||
componentConfig: component.componentConfig,
|
||||
registeredTypes: componentRegistry.getRegisteredTypes(),
|
||||
hasRenderer: componentRegistry.has(componentType),
|
||||
actualRenderer: componentRegistry.get(componentType),
|
||||
mapSize: componentRegistry.getRegisteredTypes().length,
|
||||
});
|
||||
|
||||
// 등록된 렌더러 조회
|
||||
const renderer = componentRegistry.get(componentType);
|
||||
|
||||
if (!renderer) {
|
||||
console.warn(`⚠️ 등록되지 않은 컴포넌트 타입: ${componentType}`);
|
||||
|
||||
// 폴백 렌더링 - 기본 플레이스홀더
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center rounded border-2 border-dashed border-gray-300 bg-gray-50 p-4">
|
||||
<div className="text-center">
|
||||
<div className="mb-2 text-sm font-medium text-gray-600">{component.label || component.id}</div>
|
||||
<div className="text-xs text-gray-400">미구현 컴포넌트: {componentType}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 동적 렌더링 실행
|
||||
try {
|
||||
return renderer({
|
||||
component,
|
||||
isSelected,
|
||||
onClick,
|
||||
onDragStart,
|
||||
onDragEnd,
|
||||
children,
|
||||
...props,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`❌ 컴포넌트 렌더링 실패 (${componentType}):`, error);
|
||||
|
||||
// 오류 발생 시 폴백 렌더링
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center rounded border-2 border-red-300 bg-red-50 p-4">
|
||||
<div className="text-center">
|
||||
<div className="mb-2 text-sm font-medium text-red-600">렌더링 오류</div>
|
||||
<div className="text-xs text-red-400">
|
||||
{componentType}: {error instanceof Error ? error.message : "알 수 없는 오류"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default DynamicComponentRenderer;
|
||||
57
frontend/lib/registry/components/AlertRenderer.tsx
Normal file
57
frontend/lib/registry/components/AlertRenderer.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { ComponentData } from "@/types/screen";
|
||||
import { componentRegistry, ComponentRenderer } from "../DynamicComponentRenderer";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Info, AlertTriangle, CheckCircle, XCircle } from "lucide-react";
|
||||
|
||||
// 알림 컴포넌트 렌더러
|
||||
const AlertRenderer: ComponentRenderer = ({ component, ...props }) => {
|
||||
const config = component.componentConfig || {};
|
||||
const {
|
||||
title = "알림 제목",
|
||||
message = "알림 메시지입니다.",
|
||||
type = "info", // info, warning, success, error
|
||||
showIcon = true,
|
||||
style = {},
|
||||
} = config;
|
||||
|
||||
const getAlertIcon = () => {
|
||||
switch (type) {
|
||||
case "warning":
|
||||
return <AlertTriangle className="h-4 w-4" />;
|
||||
case "success":
|
||||
return <CheckCircle className="h-4 w-4" />;
|
||||
case "error":
|
||||
return <XCircle className="h-4 w-4" />;
|
||||
default:
|
||||
return <Info className="h-4 w-4" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getAlertVariant = () => {
|
||||
switch (type) {
|
||||
case "error":
|
||||
return "destructive";
|
||||
default:
|
||||
return "default";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center p-4" style={style}>
|
||||
<Alert variant={getAlertVariant() as any} className="w-full">
|
||||
{showIcon && getAlertIcon()}
|
||||
<AlertTitle>{title}</AlertTitle>
|
||||
<AlertDescription>{message}</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 레지스트리에 등록
|
||||
componentRegistry.register("alert", AlertRenderer);
|
||||
componentRegistry.register("alert-info", AlertRenderer);
|
||||
|
||||
export { AlertRenderer };
|
||||
54
frontend/lib/registry/components/AreaRenderer.tsx
Normal file
54
frontend/lib/registry/components/AreaRenderer.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { ComponentData, AreaComponent, AreaLayoutType } from "@/types/screen";
|
||||
import { componentRegistry, ComponentRenderer } from "../DynamicComponentRenderer";
|
||||
import { Square, CreditCard, Layout, Grid3x3, Columns, Rows, SidebarOpen, Folder } from "lucide-react";
|
||||
|
||||
// 영역 레이아웃에 따른 아이콘 반환
|
||||
const getAreaIcon = (layoutType: AreaLayoutType): React.ReactNode => {
|
||||
const iconMap: Record<AreaLayoutType, React.ReactNode> = {
|
||||
container: <Square className="h-4 w-4" />,
|
||||
card: <CreditCard className="h-4 w-4" />,
|
||||
panel: <Layout className="h-4 w-4" />,
|
||||
grid: <Grid3x3 className="h-4 w-4" />,
|
||||
flex_row: <Columns className="h-4 w-4" />,
|
||||
flex_column: <Rows className="h-4 w-4" />,
|
||||
sidebar: <SidebarOpen className="h-4 w-4" />,
|
||||
section: <Folder className="h-4 w-4" />,
|
||||
};
|
||||
return iconMap[layoutType] || <Square className="h-4 w-4" />;
|
||||
};
|
||||
|
||||
// 영역 렌더링 함수
|
||||
const renderArea = (component: ComponentData, children?: React.ReactNode) => {
|
||||
const area = component as AreaComponent;
|
||||
const { title, description, layoutType = "container" } = area;
|
||||
|
||||
const renderPlaceholder = () => (
|
||||
<div className="flex h-full flex-col items-center justify-center text-center">
|
||||
{getAreaIcon(layoutType)}
|
||||
<div className="mt-2 text-sm font-medium text-gray-600">{title || "영역"}</div>
|
||||
{description && <div className="mt-1 text-xs text-gray-400">{description}</div>}
|
||||
<div className="mt-1 text-xs text-gray-400">레이아웃: {layoutType}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="relative h-full w-full rounded border border-dashed border-gray-300 bg-gray-50 p-2">
|
||||
<div className="relative h-full w-full">
|
||||
{children && React.Children.count(children) > 0 ? children : renderPlaceholder()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 영역 컴포넌트 렌더러
|
||||
const AreaRenderer: ComponentRenderer = ({ component, children, ...props }) => {
|
||||
return renderArea(component, children);
|
||||
};
|
||||
|
||||
// 레지스트리에 등록
|
||||
componentRegistry.register("area", AreaRenderer);
|
||||
|
||||
export { AreaRenderer };
|
||||
33
frontend/lib/registry/components/BadgeRenderer.tsx
Normal file
33
frontend/lib/registry/components/BadgeRenderer.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { ComponentData } from "@/types/screen";
|
||||
import { componentRegistry, ComponentRenderer } from "../DynamicComponentRenderer";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
// 뱃지 컴포넌트 렌더러
|
||||
const BadgeRenderer: ComponentRenderer = ({ component, ...props }) => {
|
||||
const config = component.componentConfig || {};
|
||||
const {
|
||||
text = "상태",
|
||||
variant = "default", // default, secondary, destructive, outline
|
||||
size = "default",
|
||||
style = {},
|
||||
} = config;
|
||||
|
||||
const badgeVariant = variant as "default" | "secondary" | "destructive" | "outline";
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center" style={style}>
|
||||
<Badge variant={badgeVariant} className="pointer-events-none">
|
||||
{text}
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 레지스트리에 등록
|
||||
componentRegistry.register("badge", BadgeRenderer);
|
||||
componentRegistry.register("badge-status", BadgeRenderer);
|
||||
|
||||
export { BadgeRenderer };
|
||||
51
frontend/lib/registry/components/BreadcrumbRenderer.tsx
Normal file
51
frontend/lib/registry/components/BreadcrumbRenderer.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { ComponentData } from "@/types/screen";
|
||||
import { componentRegistry, ComponentRenderer } from "../DynamicComponentRenderer";
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "@/components/ui/breadcrumb";
|
||||
|
||||
// 브레드크럼 컴포넌트 렌더러
|
||||
const BreadcrumbRenderer: ComponentRenderer = ({ component, ...props }) => {
|
||||
const config = component.componentConfig || {};
|
||||
const {
|
||||
items = [{ label: "홈", href: "/" }, { label: "관리자", href: "/admin" }, { label: "현재 페이지" }],
|
||||
separator = "/",
|
||||
style = {},
|
||||
} = config;
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center p-2" style={style}>
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
{items.map((item: any, index: number) => (
|
||||
<React.Fragment key={index}>
|
||||
<BreadcrumbItem>
|
||||
{index === items.length - 1 ? (
|
||||
<BreadcrumbPage>{item.label}</BreadcrumbPage>
|
||||
) : (
|
||||
<BreadcrumbLink href={item.href || "#"} className="pointer-events-none">
|
||||
{item.label}
|
||||
</BreadcrumbLink>
|
||||
)}
|
||||
</BreadcrumbItem>
|
||||
{index < items.length - 1 && <BreadcrumbSeparator />}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 레지스트리에 등록
|
||||
componentRegistry.register("breadcrumb", BreadcrumbRenderer);
|
||||
|
||||
export { BreadcrumbRenderer };
|
||||
48
frontend/lib/registry/components/ButtonRenderer.tsx
Normal file
48
frontend/lib/registry/components/ButtonRenderer.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { ComponentData } from "@/types/screen";
|
||||
import { componentRegistry, ComponentRenderer } from "../DynamicComponentRenderer";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
// 버튼 컴포넌트 렌더러
|
||||
const ButtonRenderer: ComponentRenderer = ({ component, ...props }) => {
|
||||
const config = component.componentConfig || {};
|
||||
const { text = "버튼", variant = "default", size = "default", action = "custom", style = {} } = config;
|
||||
|
||||
// 버튼 변형 매핑
|
||||
const variantMap: Record<string, any> = {
|
||||
primary: "default",
|
||||
secondary: "secondary",
|
||||
danger: "destructive",
|
||||
success: "default",
|
||||
outline: "outline",
|
||||
ghost: "ghost",
|
||||
link: "link",
|
||||
};
|
||||
|
||||
// 크기 매핑
|
||||
const sizeMap: Record<string, any> = {
|
||||
small: "sm",
|
||||
default: "default",
|
||||
large: "lg",
|
||||
};
|
||||
|
||||
const buttonVariant = variantMap[variant] || "default";
|
||||
const buttonSize = sizeMap[size] || "default";
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Button variant={buttonVariant} size={buttonSize} style={style} className="pointer-events-none" disabled>
|
||||
{text}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 레지스트리에 등록 - 모든 버튼 타입들
|
||||
componentRegistry.register("button", ButtonRenderer);
|
||||
componentRegistry.register("button-primary", ButtonRenderer);
|
||||
componentRegistry.register("button-secondary", ButtonRenderer);
|
||||
|
||||
export { ButtonRenderer };
|
||||
61
frontend/lib/registry/components/CardRenderer.tsx
Normal file
61
frontend/lib/registry/components/CardRenderer.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { ComponentData } from "@/types/screen";
|
||||
import { componentRegistry, ComponentRenderer } from "../DynamicComponentRenderer";
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardFooter } from "@/components/ui/card";
|
||||
|
||||
// 카드 컴포넌트 렌더러
|
||||
const CardRenderer: ComponentRenderer = ({ component, children, isInteractive = false, ...props }) => {
|
||||
const config = component.componentConfig || {};
|
||||
const { title = "카드 제목", content = "카드 내용 영역", showHeader = true, showFooter = false, style = {} } = config;
|
||||
|
||||
console.log("🃏 CardRenderer 렌더링:", {
|
||||
componentId: component.id,
|
||||
isInteractive,
|
||||
config,
|
||||
title,
|
||||
content,
|
||||
});
|
||||
|
||||
return (
|
||||
<Card className="h-full w-full" style={style}>
|
||||
{showHeader && (
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">{title}</CardTitle>
|
||||
</CardHeader>
|
||||
)}
|
||||
<CardContent className="flex-1 p-4">
|
||||
{children && React.Children.count(children) > 0 ? (
|
||||
children
|
||||
) : isInteractive ? (
|
||||
// 실제 할당된 화면에서는 설정된 내용 표시
|
||||
<div className="flex h-full items-start text-sm text-gray-700">
|
||||
<div className="w-full">
|
||||
<div className="mb-2 font-medium">{content}</div>
|
||||
<div className="text-xs text-gray-500">실제 할당된 화면에서 표시되는 카드입니다.</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
// 디자이너에서는 플레이스홀더 표시
|
||||
<div className="flex h-full items-center justify-center text-center">
|
||||
<div>
|
||||
<div className="text-sm text-gray-600">카드 내용 영역</div>
|
||||
<div className="mt-1 text-xs text-gray-400">컴포넌트를 여기에 배치하세요</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
{showFooter && (
|
||||
<CardFooter>
|
||||
<div className="text-sm text-gray-500">카드 푸터</div>
|
||||
</CardFooter>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
// 레지스트리에 등록
|
||||
componentRegistry.register("card", CardRenderer);
|
||||
|
||||
export { CardRenderer };
|
||||
62
frontend/lib/registry/components/ChartRenderer.tsx
Normal file
62
frontend/lib/registry/components/ChartRenderer.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { ComponentData } from "@/types/screen";
|
||||
import { componentRegistry, ComponentRenderer } from "../DynamicComponentRenderer";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { BarChart3, LineChart, PieChart } from "lucide-react";
|
||||
|
||||
// 차트 컴포넌트 렌더러
|
||||
const ChartRenderer: ComponentRenderer = ({ component, ...props }) => {
|
||||
const config = component.componentConfig || {};
|
||||
const {
|
||||
title = "차트 제목",
|
||||
chartType = "bar", // bar, line, pie
|
||||
data = [],
|
||||
style = {},
|
||||
} = config;
|
||||
|
||||
const getChartIcon = () => {
|
||||
switch (chartType) {
|
||||
case "line":
|
||||
return <LineChart className="h-8 w-8 text-blue-500" />;
|
||||
case "pie":
|
||||
return <PieChart className="h-8 w-8 text-green-500" />;
|
||||
default:
|
||||
return <BarChart3 className="h-8 w-8 text-purple-500" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getChartTypeName = () => {
|
||||
switch (chartType) {
|
||||
case "line":
|
||||
return "라인 차트";
|
||||
case "pie":
|
||||
return "파이 차트";
|
||||
default:
|
||||
return "바 차트";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="h-full w-full" style={style}>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">{title}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-1 items-center justify-center">
|
||||
<div className="text-center">
|
||||
{getChartIcon()}
|
||||
<div className="mt-2 text-sm text-gray-600">{getChartTypeName()}</div>
|
||||
<div className="mt-1 text-xs text-gray-400">미리보기 모드</div>
|
||||
{data.length > 0 && <div className="mt-2 text-xs text-gray-500">데이터 {data.length}개 항목</div>}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
// 레지스트리에 등록
|
||||
componentRegistry.register("chart", ChartRenderer);
|
||||
componentRegistry.register("chart-basic", ChartRenderer);
|
||||
|
||||
export { ChartRenderer };
|
||||
67
frontend/lib/registry/components/DashboardRenderer.tsx
Normal file
67
frontend/lib/registry/components/DashboardRenderer.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { ComponentData } from "@/types/screen";
|
||||
import { componentRegistry, ComponentRenderer } from "../DynamicComponentRenderer";
|
||||
import { LayoutGrid } from "lucide-react";
|
||||
|
||||
// 대시보드 컴포넌트 렌더러
|
||||
const DashboardRenderer: ComponentRenderer = ({ component, children, isInteractive = false, ...props }) => {
|
||||
const config = component.componentConfig || {};
|
||||
const { columns = 3, gap = 16, items = [], style = {} } = config;
|
||||
|
||||
console.log("📊 DashboardRenderer 렌더링:", {
|
||||
componentId: component.id,
|
||||
isInteractive,
|
||||
config,
|
||||
columns,
|
||||
gap,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className="h-full w-full overflow-hidden p-4"
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: `repeat(${columns}, 1fr)`,
|
||||
gap: `${gap}px`,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
boxSizing: "border-box",
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
{children && React.Children.count(children) > 0
|
||||
? children
|
||||
: // 플레이스홀더 그리드 아이템들
|
||||
Array.from({ length: columns * 2 }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`flex min-h-0 items-center justify-center rounded p-2 ${
|
||||
isInteractive
|
||||
? "border border-gray-200 bg-white shadow-sm"
|
||||
: "border-2 border-dashed border-gray-300 bg-gray-50"
|
||||
}`}
|
||||
style={{
|
||||
minWidth: 0,
|
||||
minHeight: "60px",
|
||||
maxWidth: "100%",
|
||||
}}
|
||||
>
|
||||
<div className="text-center">
|
||||
<LayoutGrid className={`mx-auto mb-2 h-6 w-6 ${isInteractive ? "text-blue-500" : "text-gray-400"}`} />
|
||||
<div className={`text-xs ${isInteractive ? "font-medium text-gray-700" : "text-gray-400"}`}>
|
||||
그리드 {index + 1}
|
||||
</div>
|
||||
{isInteractive && <div className="mt-1 text-xs text-gray-500">실제 화면</div>}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 레지스트리에 등록
|
||||
componentRegistry.register("dashboard", DashboardRenderer);
|
||||
|
||||
export { DashboardRenderer };
|
||||
43
frontend/lib/registry/components/DataTableRenderer.tsx
Normal file
43
frontend/lib/registry/components/DataTableRenderer.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { ComponentData } from "@/types/screen";
|
||||
import { componentRegistry, ComponentRenderer } from "../DynamicComponentRenderer";
|
||||
import { DataTableTemplate } from "@/components/screen/templates/DataTableTemplate";
|
||||
|
||||
// 데이터 테이블 컴포넌트 렌더러
|
||||
const DataTableRenderer: ComponentRenderer = ({ component, ...props }) => {
|
||||
const dataTableComponent = component as any; // DataTableComponent 타입
|
||||
|
||||
return (
|
||||
<DataTableTemplate
|
||||
title={dataTableComponent.title || dataTableComponent.label}
|
||||
description={`${dataTableComponent.label}을 표시하는 데이터 테이블`}
|
||||
columns={dataTableComponent.columns}
|
||||
filters={dataTableComponent.filters}
|
||||
pagination={dataTableComponent.pagination}
|
||||
actions={
|
||||
dataTableComponent.actions || {
|
||||
showSearchButton: dataTableComponent.showSearchButton ?? true,
|
||||
searchButtonText: dataTableComponent.searchButtonText || "검색",
|
||||
enableExport: dataTableComponent.enableExport ?? true,
|
||||
enableRefresh: dataTableComponent.enableRefresh ?? true,
|
||||
enableAdd: dataTableComponent.enableAdd ?? true,
|
||||
enableEdit: dataTableComponent.enableEdit ?? true,
|
||||
enableDelete: dataTableComponent.enableDelete ?? true,
|
||||
addButtonText: dataTableComponent.addButtonText || "추가",
|
||||
editButtonText: dataTableComponent.editButtonText || "수정",
|
||||
deleteButtonText: dataTableComponent.deleteButtonText || "삭제",
|
||||
}
|
||||
}
|
||||
style={component.style}
|
||||
className="h-full w-full"
|
||||
isPreview={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// 레지스트리에 등록
|
||||
componentRegistry.register("datatable", DataTableRenderer);
|
||||
|
||||
export { DataTableRenderer };
|
||||
26
frontend/lib/registry/components/FileRenderer.tsx
Normal file
26
frontend/lib/registry/components/FileRenderer.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { ComponentData } from "@/types/screen";
|
||||
import { componentRegistry, ComponentRenderer } from "../DynamicComponentRenderer";
|
||||
import { File } from "lucide-react";
|
||||
|
||||
// 파일 컴포넌트 렌더러
|
||||
const FileRenderer: ComponentRenderer = ({ component, ...props }) => {
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="pointer-events-none flex-1 rounded border-2 border-dashed border-gray-300 bg-gray-50 p-4">
|
||||
<div className="flex h-full flex-col items-center justify-center text-center">
|
||||
<File className="mb-2 h-8 w-8 text-gray-400" />
|
||||
<p className="text-sm text-gray-600">파일 업로드 영역</p>
|
||||
<p className="mt-1 text-xs text-gray-400">미리보기 모드</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 레지스트리에 등록
|
||||
componentRegistry.register("file", FileRenderer);
|
||||
|
||||
export { FileRenderer };
|
||||
49
frontend/lib/registry/components/FilterDropdownRenderer.tsx
Normal file
49
frontend/lib/registry/components/FilterDropdownRenderer.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { ComponentData } from "@/types/screen";
|
||||
import { componentRegistry, ComponentRenderer } from "../DynamicComponentRenderer";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Filter } from "lucide-react";
|
||||
|
||||
// 필터 드롭다운 컴포넌트 렌더러
|
||||
const FilterDropdownRenderer: ComponentRenderer = ({ component, ...props }) => {
|
||||
const config = component.componentConfig || {};
|
||||
const {
|
||||
label = "필터",
|
||||
placeholder = "필터를 선택하세요",
|
||||
options = [
|
||||
{ label: "전체", value: "all" },
|
||||
{ label: "활성", value: "active" },
|
||||
{ label: "비활성", value: "inactive" },
|
||||
],
|
||||
showIcon = true,
|
||||
style = {},
|
||||
} = config;
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center gap-2 p-2" style={style}>
|
||||
{showIcon && <Filter className="h-4 w-4 text-gray-500" />}
|
||||
<div className="flex-1">
|
||||
<Select disabled>
|
||||
<SelectTrigger className="pointer-events-none">
|
||||
<SelectValue placeholder={placeholder} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{options.map((option: any, index: number) => (
|
||||
<SelectItem key={option.value || index} value={option.value || index.toString()}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 레지스트리에 등록
|
||||
componentRegistry.register("filter", FilterDropdownRenderer);
|
||||
componentRegistry.register("filter-dropdown", FilterDropdownRenderer);
|
||||
|
||||
export { FilterDropdownRenderer };
|
||||
19
frontend/lib/registry/components/GroupRenderer.tsx
Normal file
19
frontend/lib/registry/components/GroupRenderer.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { ComponentData } from "@/types/screen";
|
||||
import { componentRegistry, ComponentRenderer } from "../DynamicComponentRenderer";
|
||||
|
||||
// 그룹 컴포넌트 렌더러
|
||||
const GroupRenderer: ComponentRenderer = ({ component, children, ...props }) => {
|
||||
return (
|
||||
<div className="relative h-full w-full">
|
||||
<div className="absolute inset-0">{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 레지스트리에 등록
|
||||
componentRegistry.register("group", GroupRenderer);
|
||||
|
||||
export { GroupRenderer };
|
||||
41
frontend/lib/registry/components/LoadingRenderer.tsx
Normal file
41
frontend/lib/registry/components/LoadingRenderer.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { ComponentData } from "@/types/screen";
|
||||
import { componentRegistry, ComponentRenderer } from "../DynamicComponentRenderer";
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
// 로딩 스피너 컴포넌트 렌더러
|
||||
const LoadingRenderer: ComponentRenderer = ({ component, ...props }) => {
|
||||
const config = component.componentConfig || {};
|
||||
const {
|
||||
text = "로딩 중...",
|
||||
size = "default", // small, default, large
|
||||
showText = true,
|
||||
style = {},
|
||||
} = config;
|
||||
|
||||
const getSizeClass = () => {
|
||||
switch (size) {
|
||||
case "small":
|
||||
return "h-4 w-4";
|
||||
case "large":
|
||||
return "h-8 w-8";
|
||||
default:
|
||||
return "h-6 w-6";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center gap-2" style={style}>
|
||||
<Loader2 className={`animate-spin text-blue-600 ${getSizeClass()}`} />
|
||||
{showText && <div className="text-sm text-gray-600">{text}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 레지스트리에 등록
|
||||
componentRegistry.register("loading", LoadingRenderer);
|
||||
componentRegistry.register("loading-spinner", LoadingRenderer);
|
||||
|
||||
export { LoadingRenderer };
|
||||
89
frontend/lib/registry/components/PaginationRenderer.tsx
Normal file
89
frontend/lib/registry/components/PaginationRenderer.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { ComponentData } from "@/types/screen";
|
||||
import { componentRegistry, ComponentRenderer } from "../DynamicComponentRenderer";
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationEllipsis,
|
||||
PaginationItem,
|
||||
PaginationLink,
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
} from "@/components/ui/pagination";
|
||||
|
||||
// 페이지네이션 컴포넌트 렌더러
|
||||
const PaginationRenderer: ComponentRenderer = ({ component, ...props }) => {
|
||||
const config = component.componentConfig || {};
|
||||
const { currentPage = 1, totalPages = 10, showPrevNext = true, showEllipsis = true, style = {} } = config;
|
||||
|
||||
const generatePageNumbers = () => {
|
||||
const pages = [];
|
||||
const maxVisible = 5;
|
||||
|
||||
if (totalPages <= maxVisible) {
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
} else {
|
||||
pages.push(1);
|
||||
if (currentPage > 3) {
|
||||
pages.push("ellipsis1");
|
||||
}
|
||||
|
||||
const start = Math.max(2, currentPage - 1);
|
||||
const end = Math.min(totalPages - 1, currentPage + 1);
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
|
||||
if (currentPage < totalPages - 2) {
|
||||
pages.push("ellipsis2");
|
||||
}
|
||||
pages.push(totalPages);
|
||||
}
|
||||
|
||||
return pages;
|
||||
};
|
||||
|
||||
const pageNumbers = generatePageNumbers();
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center" style={style}>
|
||||
<Pagination>
|
||||
<PaginationContent>
|
||||
{showPrevNext && (
|
||||
<PaginationItem>
|
||||
<PaginationPrevious href="#" className="pointer-events-none" />
|
||||
</PaginationItem>
|
||||
)}
|
||||
|
||||
{pageNumbers.map((page, index) => (
|
||||
<PaginationItem key={index}>
|
||||
{typeof page === "string" && page.startsWith("ellipsis") ? (
|
||||
showEllipsis && <PaginationEllipsis />
|
||||
) : (
|
||||
<PaginationLink href="#" isActive={page === currentPage} className="pointer-events-none">
|
||||
{page}
|
||||
</PaginationLink>
|
||||
)}
|
||||
</PaginationItem>
|
||||
))}
|
||||
|
||||
{showPrevNext && (
|
||||
<PaginationItem>
|
||||
<PaginationNext href="#" className="pointer-events-none" />
|
||||
</PaginationItem>
|
||||
)}
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 레지스트리에 등록
|
||||
componentRegistry.register("pagination", PaginationRenderer);
|
||||
|
||||
export { PaginationRenderer };
|
||||
57
frontend/lib/registry/components/PanelRenderer.tsx
Normal file
57
frontend/lib/registry/components/PanelRenderer.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { ComponentData } from "@/types/screen";
|
||||
import { componentRegistry, ComponentRenderer } from "../DynamicComponentRenderer";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ChevronDown, ChevronUp } from "lucide-react";
|
||||
|
||||
// 접을 수 있는 패널 컴포넌트 렌더러
|
||||
const PanelRenderer: ComponentRenderer = ({ component, children, ...props }) => {
|
||||
const config = component.componentConfig || {};
|
||||
const { title = "패널 제목", collapsible = true, defaultExpanded = true, style = {} } = config;
|
||||
|
||||
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
|
||||
|
||||
return (
|
||||
<Card className="h-full w-full" style={style}>
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-lg">{title}</CardTitle>
|
||||
{collapsible && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="pointer-events-none h-6 w-6 p-0"
|
||||
disabled
|
||||
>
|
||||
{isExpanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
{isExpanded && (
|
||||
<CardContent className="flex-1">
|
||||
{children && React.Children.count(children) > 0 ? (
|
||||
children
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center text-center">
|
||||
<div>
|
||||
<div className="text-sm text-gray-600">패널 내용 영역</div>
|
||||
<div className="mt-1 text-xs text-gray-400">컴포넌트를 여기에 배치하세요</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
// 레지스트리에 등록
|
||||
componentRegistry.register("panel", PanelRenderer);
|
||||
componentRegistry.register("panel-collapsible", PanelRenderer);
|
||||
|
||||
export { PanelRenderer };
|
||||
55
frontend/lib/registry/components/ProgressBarRenderer.tsx
Normal file
55
frontend/lib/registry/components/ProgressBarRenderer.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { ComponentData } from "@/types/screen";
|
||||
import { componentRegistry, ComponentRenderer } from "../DynamicComponentRenderer";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
|
||||
// 진행률 바 컴포넌트 렌더러
|
||||
const ProgressBarRenderer: ComponentRenderer = ({ component, ...props }) => {
|
||||
const config = component.componentConfig || {};
|
||||
const {
|
||||
label = "진행률",
|
||||
value = 65,
|
||||
max = 100,
|
||||
showPercentage = true,
|
||||
showValue = true,
|
||||
color = "#3b82f6",
|
||||
style = {},
|
||||
} = config;
|
||||
|
||||
const percentage = Math.round((value / max) * 100);
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col justify-center p-4" style={style}>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-gray-700">{label}</span>
|
||||
<div className="flex items-center gap-2 text-sm text-gray-600">
|
||||
{showValue && (
|
||||
<span>
|
||||
{value}/{max}
|
||||
</span>
|
||||
)}
|
||||
{showPercentage && <span>({percentage}%)</span>}
|
||||
</div>
|
||||
</div>
|
||||
<Progress
|
||||
value={percentage}
|
||||
className="h-2"
|
||||
style={
|
||||
{
|
||||
"--progress-background": color,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 레지스트리에 등록
|
||||
componentRegistry.register("progress", ProgressBarRenderer);
|
||||
componentRegistry.register("progress-bar", ProgressBarRenderer);
|
||||
|
||||
export { ProgressBarRenderer };
|
||||
34
frontend/lib/registry/components/SearchBoxRenderer.tsx
Normal file
34
frontend/lib/registry/components/SearchBoxRenderer.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { ComponentData } from "@/types/screen";
|
||||
import { componentRegistry, ComponentRenderer } from "../DynamicComponentRenderer";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Search } from "lucide-react";
|
||||
|
||||
// 검색 박스 컴포넌트 렌더러
|
||||
const SearchBoxRenderer: ComponentRenderer = ({ component, ...props }) => {
|
||||
const config = component.componentConfig || {};
|
||||
const { placeholder = "검색어를 입력하세요...", showButton = true, buttonText = "검색", style = {} } = config;
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center gap-2 p-2" style={style}>
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-gray-400" />
|
||||
<Input placeholder={placeholder} className="pointer-events-none pl-10" disabled />
|
||||
</div>
|
||||
{showButton && (
|
||||
<Button className="pointer-events-none" disabled>
|
||||
{buttonText}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 레지스트리에 등록
|
||||
componentRegistry.register("search", SearchBoxRenderer);
|
||||
componentRegistry.register("search-box", SearchBoxRenderer);
|
||||
|
||||
export { SearchBoxRenderer };
|
||||
68
frontend/lib/registry/components/StatsCardRenderer.tsx
Normal file
68
frontend/lib/registry/components/StatsCardRenderer.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { ComponentData } from "@/types/screen";
|
||||
import { componentRegistry, ComponentRenderer } from "../DynamicComponentRenderer";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { TrendingUp, TrendingDown, Minus } from "lucide-react";
|
||||
|
||||
// 통계 카드 컴포넌트 렌더러
|
||||
const StatsCardRenderer: ComponentRenderer = ({ component, ...props }) => {
|
||||
const config = component.componentConfig || {};
|
||||
const {
|
||||
title = "통계 제목",
|
||||
value = "1,234",
|
||||
change = "+12.5%",
|
||||
trend = "up", // up, down, neutral
|
||||
description = "전월 대비",
|
||||
style = {},
|
||||
} = config;
|
||||
|
||||
const getTrendIcon = () => {
|
||||
switch (trend) {
|
||||
case "up":
|
||||
return <TrendingUp className="h-4 w-4 text-green-600" />;
|
||||
case "down":
|
||||
return <TrendingDown className="h-4 w-4 text-red-600" />;
|
||||
default:
|
||||
return <Minus className="h-4 w-4 text-gray-600" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getTrendColor = () => {
|
||||
switch (trend) {
|
||||
case "up":
|
||||
return "text-green-600";
|
||||
case "down":
|
||||
return "text-red-600";
|
||||
default:
|
||||
return "text-gray-600";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="h-full w-full" style={style}>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-gray-600">{title}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-2xl font-bold">{value}</div>
|
||||
<div className={`flex items-center gap-1 text-sm ${getTrendColor()}`}>
|
||||
{getTrendIcon()}
|
||||
<span>{change}</span>
|
||||
<span className="text-gray-500">{description}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
// 레지스트리에 등록
|
||||
componentRegistry.register("stats", StatsCardRenderer);
|
||||
componentRegistry.register("stats-card", StatsCardRenderer);
|
||||
|
||||
export { StatsCardRenderer };
|
||||
55
frontend/lib/registry/components/TabsRenderer.tsx
Normal file
55
frontend/lib/registry/components/TabsRenderer.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { ComponentData } from "@/types/screen";
|
||||
import { componentRegistry, ComponentRenderer } from "../DynamicComponentRenderer";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
|
||||
// 탭 컴포넌트 렌더러
|
||||
const TabsRenderer: ComponentRenderer = ({ component, children, ...props }) => {
|
||||
const config = component.componentConfig || {};
|
||||
const {
|
||||
tabs = [
|
||||
{ id: "tab1", label: "탭 1", content: "첫 번째 탭 내용" },
|
||||
{ id: "tab2", label: "탭 2", content: "두 번째 탭 내용" },
|
||||
{ id: "tab3", label: "탭 3", content: "세 번째 탭 내용" },
|
||||
],
|
||||
defaultTab = "tab1",
|
||||
orientation = "horizontal", // horizontal, vertical
|
||||
style = {},
|
||||
} = config;
|
||||
|
||||
return (
|
||||
<div className="h-full w-full p-2" style={style}>
|
||||
<Tabs defaultValue={defaultTab} orientation={orientation} className="h-full">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
{tabs.map((tab: any) => (
|
||||
<TabsTrigger key={tab.id} value={tab.id} className="pointer-events-none" disabled>
|
||||
{tab.label}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
{tabs.map((tab: any) => (
|
||||
<TabsContent key={tab.id} value={tab.id} className="mt-4 flex-1">
|
||||
{children && React.Children.count(children) > 0 ? (
|
||||
children
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center rounded border border-dashed border-gray-300 bg-gray-50">
|
||||
<div className="text-center">
|
||||
<div className="text-sm text-gray-600">{tab.content}</div>
|
||||
<div className="mt-1 text-xs text-gray-400">탭 내용 영역</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 레지스트리에 등록
|
||||
componentRegistry.register("tabs", TabsRenderer);
|
||||
componentRegistry.register("tabs-horizontal", TabsRenderer);
|
||||
|
||||
export { TabsRenderer };
|
||||
80
frontend/lib/registry/components/WidgetRenderer.tsx
Normal file
80
frontend/lib/registry/components/WidgetRenderer.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { ComponentData, WidgetComponent } from "@/types/screen";
|
||||
import { componentRegistry, ComponentRenderer } from "../DynamicComponentRenderer";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { DynamicWebTypeRenderer } from "../DynamicWebTypeRenderer";
|
||||
|
||||
// 위젯 컴포넌트 렌더러
|
||||
const WidgetRenderer: ComponentRenderer = ({ component, ...props }) => {
|
||||
// 위젯 컴포넌트가 아닌 경우 빈 div 반환
|
||||
if (component.type !== "widget") {
|
||||
return <div className="text-xs text-gray-500">위젯이 아닙니다</div>;
|
||||
}
|
||||
|
||||
const widget = component as WidgetComponent;
|
||||
const { widgetType, label, placeholder, required, readonly, columnName, style } = widget;
|
||||
|
||||
// 디버깅: 실제 widgetType 값 확인
|
||||
console.log("WidgetRenderer - widgetType:", widgetType, "columnName:", columnName);
|
||||
|
||||
// 사용자가 테두리를 설정했는지 확인
|
||||
const hasCustomBorder = style && (style.borderWidth || style.borderStyle || style.borderColor || style.border);
|
||||
|
||||
// 기본 테두리 제거 여부 결정 - Shadcn UI 기본 border 클래스를 덮어쓰기
|
||||
const borderClass = hasCustomBorder ? "!border-0" : "";
|
||||
|
||||
const commonProps = {
|
||||
placeholder: placeholder || "입력하세요...",
|
||||
disabled: readonly,
|
||||
required: required,
|
||||
className: `w-full h-full ${borderClass}`,
|
||||
};
|
||||
|
||||
// 동적 웹타입 렌더링 사용
|
||||
if (widgetType) {
|
||||
try {
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="pointer-events-none flex-1">
|
||||
<DynamicWebTypeRenderer
|
||||
webType={widgetType}
|
||||
props={{
|
||||
...commonProps,
|
||||
component: widget,
|
||||
value: undefined, // 미리보기이므로 값은 없음
|
||||
readonly: readonly,
|
||||
}}
|
||||
config={widget.webTypeConfig}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(`웹타입 "${widgetType}" 렌더링 실패:`, error);
|
||||
// 오류 발생 시 폴백으로 기본 input 렌더링
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="pointer-events-none flex-1">
|
||||
<Input type="text" {...commonProps} placeholder={`${widgetType} (렌더링 오류)`} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 웹타입이 없는 경우 기본 input 렌더링 (하위 호환성)
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="pointer-events-none flex-1">
|
||||
<Input type="text" {...commonProps} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 레지스트리에 등록
|
||||
componentRegistry.register("widget", WidgetRenderer);
|
||||
|
||||
export { WidgetRenderer };
|
||||
56
frontend/lib/registry/components/index.ts
Normal file
56
frontend/lib/registry/components/index.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
// 컴포넌트 렌더러들을 자동으로 등록하는 인덱스 파일
|
||||
|
||||
// 기존 컴포넌트 렌더러들 import
|
||||
import "./AreaRenderer";
|
||||
import "./GroupRenderer";
|
||||
import "./WidgetRenderer";
|
||||
import "./FileRenderer";
|
||||
import "./DataTableRenderer";
|
||||
|
||||
// ACTION 카테고리
|
||||
import "./ButtonRenderer";
|
||||
|
||||
// DATA 카테고리
|
||||
import "./StatsCardRenderer";
|
||||
import "./ProgressBarRenderer";
|
||||
import "./ChartRenderer";
|
||||
|
||||
// FEEDBACK 카테고리
|
||||
import "./AlertRenderer";
|
||||
import "./BadgeRenderer";
|
||||
import "./LoadingRenderer";
|
||||
|
||||
// INPUT 카테고리
|
||||
import "./SearchBoxRenderer";
|
||||
import "./FilterDropdownRenderer";
|
||||
|
||||
// LAYOUT 카테고리
|
||||
import "./CardRenderer";
|
||||
import "./DashboardRenderer";
|
||||
import "./PanelRenderer";
|
||||
|
||||
// NAVIGATION 카테고리
|
||||
import "./BreadcrumbRenderer";
|
||||
import "./TabsRenderer";
|
||||
import "./PaginationRenderer";
|
||||
|
||||
export * from "./AreaRenderer";
|
||||
export * from "./GroupRenderer";
|
||||
export * from "./WidgetRenderer";
|
||||
export * from "./FileRenderer";
|
||||
export * from "./DataTableRenderer";
|
||||
export * from "./ButtonRenderer";
|
||||
export * from "./StatsCardRenderer";
|
||||
export * from "./ProgressBarRenderer";
|
||||
export * from "./ChartRenderer";
|
||||
export * from "./AlertRenderer";
|
||||
export * from "./BadgeRenderer";
|
||||
export * from "./LoadingRenderer";
|
||||
export * from "./SearchBoxRenderer";
|
||||
export * from "./FilterDropdownRenderer";
|
||||
export * from "./CardRenderer";
|
||||
export * from "./DashboardRenderer";
|
||||
export * from "./PanelRenderer";
|
||||
export * from "./BreadcrumbRenderer";
|
||||
export * from "./TabsRenderer";
|
||||
export * from "./PaginationRenderer";
|
||||
Reference in New Issue
Block a user