ERP-node/frontend/lib/meta-components/Display/DisplayRenderer.tsx

199 lines
5.3 KiB
TypeScript
Raw Normal View History

2026-03-01 03:39:00 +09:00
"use client";
/**
* Display
* - config.displayType에 :
* - text: <p>
* - heading: <h2>~<h4>
* - divider: <hr>
* - badge: shadcn Badge
* - alert: shadcn Alert
* - stat: 통계 ( + )
* - config.dataBinding이 formData에서
* - 모드: 대체
*/
import React from "react";
import { cn } from "@/lib/utils";
import { Separator } from "@/components/ui/separator";
import { Badge } from "@/components/ui/badge";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { TrendingUp, TrendingDown } from "lucide-react";
interface DisplayRendererProps {
id: string;
config: {
displayType: "text" | "heading" | "divider" | "badge" | "alert" | "stat";
text?: {
content: string;
size?: "xs" | "sm" | "md" | "lg" | "xl";
weight?: "normal" | "medium" | "semibold" | "bold";
align?: "left" | "center" | "right";
};
heading?: {
content: string;
level: 1 | 2 | 3 | 4 | 5 | 6;
};
badge?: {
text: string;
variant?: "default" | "secondary" | "outline" | "destructive";
};
alert?: {
title?: string;
message: string;
variant?: "default" | "destructive";
};
stat?: {
label: string;
value: string | number;
change?: string;
changeType?: "increase" | "decrease" | "neutral";
};
dataBinding?: string; // formData에서 값 바인딩 시 키
};
formData?: Record<string, any>;
isDesignMode?: boolean;
className?: string;
}
export function DisplayRenderer({
id,
config,
formData,
isDesignMode = false,
className,
}: DisplayRendererProps) {
const { displayType } = config;
// 데이터 바인딩 처리
const getBoundValue = (defaultValue: any) => {
if (config.dataBinding && formData) {
return formData[config.dataBinding] ?? defaultValue;
}
return defaultValue;
};
// text: <p> + 크기/굵기/정렬
if (displayType === "text" && config.text) {
const { content, size = "md", weight = "normal", align = "left" } = config.text;
const boundContent = getBoundValue(content);
const sizeClass = {
xs: "text-xs",
sm: "text-sm",
md: "text-base",
lg: "text-lg",
xl: "text-xl",
}[size];
const weightClass = {
normal: "font-normal",
medium: "font-medium",
semibold: "font-semibold",
bold: "font-bold",
}[weight];
const alignClass = {
left: "text-left",
center: "text-center",
right: "text-right",
}[align];
return (
<p className={cn(sizeClass, weightClass, alignClass, className)}>
{isDesignMode && !boundContent ? "(텍스트)" : boundContent}
</p>
);
}
// heading: <h1>~<h6>
if (displayType === "heading" && config.heading) {
const { content, level } = config.heading;
const boundContent = getBoundValue(content);
const Tag = `h${level}` as keyof JSX.IntrinsicElements;
const sizeClass = {
1: "text-3xl",
2: "text-2xl",
3: "text-xl",
4: "text-lg",
5: "text-base",
6: "text-sm",
}[level];
return (
<Tag className={cn(sizeClass, "font-bold", className)}>
{isDesignMode && !boundContent ? "(제목)" : boundContent}
</Tag>
);
}
// divider: shadcn Separator
if (displayType === "divider") {
return <Separator className={cn("my-4", className)} />;
}
// badge: shadcn Badge
if (displayType === "badge" && config.badge) {
const { text, variant = "default" } = config.badge;
const boundText = getBoundValue(text);
return (
<Badge variant={variant} className={className}>
{isDesignMode && !boundText ? "(뱃지)" : boundText}
</Badge>
);
}
// alert: shadcn Alert
if (displayType === "alert" && config.alert) {
const { title, message, variant = "default" } = config.alert;
const boundMessage = getBoundValue(message);
return (
<Alert variant={variant} className={className}>
{title && <AlertTitle>{title}</AlertTitle>}
<AlertDescription>
{isDesignMode && !boundMessage ? "(알림 메시지)" : boundMessage}
</AlertDescription>
</Alert>
);
}
// stat: 통계 카드
if (displayType === "stat" && config.stat) {
const { label, value, change, changeType = "neutral" } = config.stat;
const boundValue = getBoundValue(value);
const icon =
changeType === "increase" ? (
<TrendingUp className="h-4 w-4 text-green-600" />
) : changeType === "decrease" ? (
<TrendingDown className="h-4 w-4 text-red-600" />
) : null;
return (
<div className={cn("rounded-lg border bg-card p-6", className)}>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">{label}</span>
{change && (
<div className="flex items-center gap-1">
{icon}
<span className="text-xs text-muted-foreground">{change}</span>
</div>
)}
</div>
<div className="mt-2 text-3xl font-bold">
{isDesignMode && !boundValue ? "0" : boundValue}
</div>
</div>
);
}
return (
<div className="text-destructive">
Unknown display type: {displayType}
</div>
);
}