199 lines
5.3 KiB
TypeScript
199 lines
5.3 KiB
TypeScript
"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>
|
|
);
|
|
}
|