컴포넌트 화면편집기에 배치

This commit is contained in:
kjs
2025-09-10 14:09:32 +09:00
parent 3bf694ce24
commit 01860df8d7
56 changed files with 4572 additions and 778 deletions

View 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;

View 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 };

View 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 };

View 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 };

View 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 };

View 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 };

View 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 };

View 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 };

View 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 };

View 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 };

View 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 };

View 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 };

View 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 };

View 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 };

View 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 };

View 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 };

View 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 };

View 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 };

View 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 };

View 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 };

View 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 };

View 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";