"use client";
import type { CardRendererProps, QueryResult } from "./types";
import type {
CardLayoutConfig,
CardLayoutRow,
CardElement,
CardHeaderElement,
CardDataCellElement,
CardDividerElement,
CardBadgeElement,
CardImageElement,
CardNumberElement,
CardDateElement,
CardLinkElement,
CardStatusElement,
CardSpacerElement,
CardStaticTextElement,
} from "@/types/report";
import * as LucideIcons from "lucide-react";
// ─────────────────────────────────────────────────────────────────────────────
// 기존 cardItems 방식 렌더러 (하위 호환)
// ─────────────────────────────────────────────────────────────────────────────
function CardListRenderer({ component, getQueryResult }: CardRendererProps) {
const cardTitle = component.cardTitle || "정보 카드";
const cardItems = component.cardItems || [];
const labelWidth = component.labelWidth || 80;
const showCardTitle = component.showCardTitle !== false;
const titleFontSize = component.titleFontSize || 14;
const labelFontSize = component.labelFontSize || 13;
const valueFontSize = component.valueFontSize || 13;
const titleColor = component.titleColor || "#1e40af";
const labelColor = component.labelColor || "#374151";
const valueColor = component.valueColor || "#000000";
const getCardItemValue = (item: {
label: string;
value: string;
fieldName?: string;
}) => {
if (item.fieldName && component.queryId) {
const queryResult = getQueryResult(component.queryId);
if (queryResult && queryResult.rows && queryResult.rows.length > 0) {
const row = queryResult.rows[0];
return row[item.fieldName] !== undefined
? String(row[item.fieldName])
: item.value;
}
}
return item.value;
};
return (
{showCardTitle && (
<>
{cardTitle}
>
)}
{cardItems.map(
(
item: { label: string; value: string; fieldName?: string },
index: number,
) => (
{item.label}
{getCardItemValue(item)}
),
)}
);
}
// ─────────────────────────────────────────────────────────────────────────────
// v3 요소별 렌더러
// ─────────────────────────────────────────────────────────────────────────────
interface ElementRendererProps {
element: CardElement;
getQueryResult: (queryId: string) => QueryResult | null;
queryId?: string;
config: CardLayoutConfig;
}
function CardHeaderRenderer({
element,
config,
}: {
element: CardHeaderElement;
config: CardLayoutConfig;
}) {
const titleFontSize =
element.titleFontSize || config.headerTitleFontSize || 14;
const titleColor = element.titleColor || config.headerTitleColor || "#1e40af";
const iconColor = element.iconColor || titleColor;
const IconComponent = element.icon
? (LucideIcons as Record>)[element.icon]
: null;
return (
{IconComponent && (
)}
{element.title}
);
}
function CardDataCellRenderer({
element,
getQueryResult,
queryId,
config,
}: {
element: CardDataCellElement;
getQueryResult: (queryId: string) => QueryResult | null;
queryId?: string;
config: CardLayoutConfig;
}) {
const labelFontSize =
element.labelFontSize || config.labelFontSize || 13;
const labelColor = element.labelColor || config.labelColor || "#374151";
const valueFontSize =
element.valueFontSize || config.valueFontSize || 13;
const valueColor = element.valueColor || config.valueColor || "#000000";
const getValue = (): string => {
if (element.columnName && queryId) {
const queryResult = getQueryResult(queryId);
if (queryResult && queryResult.rows && queryResult.rows.length > 0) {
const row = queryResult.rows[0];
const value = row[element.columnName];
return value !== undefined && value !== null ? String(value) : "";
}
}
return "";
};
const value = getValue() || "-";
if (element.direction === "vertical") {
return (
{element.label}
{value}
);
}
return (
{element.label}
{value}
);
}
function CardDividerRenderer({
element,
config,
}: {
element: CardDividerElement;
config: CardLayoutConfig;
}) {
const thickness = element.thickness || config.dividerThickness || 1;
const color = element.color || config.dividerColor || "#e5e7eb";
const style = element.style || "solid";
return (
);
}
function CardBadgeRenderer({
element,
getQueryResult,
queryId,
}: {
element: CardBadgeElement;
getQueryResult: (queryId: string) => QueryResult | null;
queryId?: string;
config: CardLayoutConfig;
}) {
const getValue = (): string => {
if (element.columnName && queryId) {
const queryResult = getQueryResult(queryId);
if (queryResult && queryResult.rows && queryResult.rows.length > 0) {
const row = queryResult.rows[0];
const value = row[element.columnName];
return value !== undefined && value !== null ? String(value) : "";
}
}
return "";
};
const value = getValue();
const bgColor = element.colorMap?.[value] || "#e5e7eb";
return (
{element.label && (
{element.label}
)}
{value || "-"}
);
}
function getFieldValue(
columnName: string | undefined,
queryId: string | undefined,
getQueryResult: (id: string) => QueryResult | null,
): string {
if (!columnName || !queryId) return "";
const result = getQueryResult(queryId);
if (result?.rows?.length) {
const val = result.rows[0][columnName];
return val !== undefined && val !== null ? String(val) : "";
}
return "";
}
function formatNumber(raw: string, format?: string, suffix?: string): string {
const num = parseFloat(raw);
if (isNaN(num)) return raw || "-";
if (format === "comma" || format === "currency") {
const formatted = num.toLocaleString("ko-KR");
return format === "currency" ? `${formatted}${suffix || "원"}` : formatted;
}
return raw;
}
function CardImageRenderer({
element,
getQueryResult,
queryId,
}: {
element: CardImageElement;
getQueryResult: (id: string) => QueryResult | null;
queryId?: string;
}) {
const url = getFieldValue(element.columnName, queryId, getQueryResult);
return (
{url ? (

) : (
이미지
)}
);
}
function CardNumberRenderer({
element,
getQueryResult,
queryId,
config,
}: {
element: CardNumberElement;
getQueryResult: (id: string) => QueryResult | null;
queryId?: string;
config: CardLayoutConfig;
}) {
const raw = getFieldValue(element.columnName, queryId, getQueryResult);
const value = formatNumber(raw, element.numberFormat, element.currencySuffix);
const labelFontSize = element.labelFontSize || config.labelFontSize || 13;
const labelColor = element.labelColor || config.labelColor || "#374151";
const valueFontSize = element.valueFontSize || config.valueFontSize || 13;
const valueColor = element.valueColor || config.valueColor || "#000000";
return (
{element.label}
{value}
);
}
function CardDateRenderer({
element,
getQueryResult,
queryId,
config,
}: {
element: CardDateElement;
getQueryResult: (id: string) => QueryResult | null;
queryId?: string;
config: CardLayoutConfig;
}) {
const raw = getFieldValue(element.columnName, queryId, getQueryResult);
const labelFontSize = element.labelFontSize || config.labelFontSize || 13;
const labelColor = element.labelColor || config.labelColor || "#374151";
const valueFontSize = element.valueFontSize || config.valueFontSize || 13;
const valueColor = element.valueColor || config.valueColor || "#000000";
let displayValue = raw || "-";
if (raw && element.dateFormat) {
try {
const d = new Date(raw);
if (!isNaN(d.getTime())) {
displayValue = element.dateFormat
.replace("YYYY", String(d.getFullYear()))
.replace("MM", String(d.getMonth() + 1).padStart(2, "0"))
.replace("DD", String(d.getDate()).padStart(2, "0"));
}
} catch {
displayValue = raw;
}
}
return (
{element.label}
{displayValue}
);
}
function CardLinkRenderer({
element,
getQueryResult,
queryId,
}: {
element: CardLinkElement;
getQueryResult: (id: string) => QueryResult | null;
queryId?: string;
}) {
const url = getFieldValue(element.columnName, queryId, getQueryResult);
const text = element.linkText || url || element.label;
return (
{element.label}
{url ? (
{text}
) : (
-
)}
);
}
function CardStatusRenderer({
element,
getQueryResult,
queryId,
}: {
element: CardStatusElement;
getQueryResult: (id: string) => QueryResult | null;
queryId?: string;
}) {
const value = getFieldValue(element.columnName, queryId, getQueryResult);
const mapping = element.statusMappings?.find((m) => m.value === value);
const dotColor = mapping?.color || "#9ca3af";
const label = mapping?.label || value || "-";
return (
{label}
);
}
function CardStaticTextRenderer({ element }: { element: CardStaticTextElement }) {
return (
{element.text}
);
}
function CardElementRenderer({
element,
getQueryResult,
queryId,
config,
}: ElementRendererProps) {
switch (element.type) {
case "header":
return ;
case "dataCell":
return (
);
case "divider":
return ;
case "badge":
return (
);
case "image":
return ;
case "number":
return ;
case "date":
return ;
case "link":
return ;
case "status":
return ;
case "spacer":
return ;
case "staticText":
return ;
default:
return null;
}
}
// ─────────────────────────────────────────────────────────────────────────────
// v3 그리드 렌더러
// ─────────────────────────────────────────────────────────────────────────────
function CardGridRenderer({
config,
getQueryResult,
queryId,
}: {
config: CardLayoutConfig;
getQueryResult: (queryId: string) => QueryResult | null;
queryId?: string;
}) {
return (
0
? { borderLeft: `${config.accentBorderWidth}px solid ${config.accentBorderColor || "#3b82f6"}` }
: {}),
}}
>
{config.rows.map((row: CardLayoutRow) => (
{row.elements.map((element: CardElement) => (
))}
))}
);
}
// ─────────────────────────────────────────────────────────────────────────────
// 메인 컴포넌트
// ─────────────────────────────────────────────────────────────────────────────
export function CardRenderer({ component, getQueryResult }: CardRendererProps) {
const effectiveQueryId = component.queryId || `card_${component.id}`;
if (component.cardLayoutConfig) {
return (
);
}
return (
);
}