2026-03-09 13:15:41 +09:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import React, { useEffect, useState, useCallback } from "react";
|
|
|
|
|
import { ComponentRendererProps } from "@/types/component";
|
|
|
|
|
import { StatusCountConfig, StatusCountItem, STATUS_COLOR_MAP } from "./types";
|
|
|
|
|
import { apiClient } from "@/lib/api/client";
|
|
|
|
|
|
|
|
|
|
export interface StatusCountComponentProps extends ComponentRendererProps {}
|
|
|
|
|
|
|
|
|
|
export const StatusCountComponent: React.FC<StatusCountComponentProps> = ({
|
|
|
|
|
component,
|
|
|
|
|
isDesignMode = false,
|
|
|
|
|
isSelected = false,
|
|
|
|
|
formData,
|
|
|
|
|
...props
|
|
|
|
|
}) => {
|
|
|
|
|
const config = (component.componentConfig || {}) as StatusCountConfig;
|
|
|
|
|
const [counts, setCounts] = useState<Record<string, number>>({});
|
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
title,
|
|
|
|
|
tableName,
|
|
|
|
|
statusColumn = "status",
|
|
|
|
|
relationColumn,
|
|
|
|
|
parentColumn,
|
|
|
|
|
items = [],
|
|
|
|
|
cardSize = "md",
|
|
|
|
|
} = config;
|
|
|
|
|
|
|
|
|
|
const parentValue = formData?.[parentColumn || relationColumn];
|
|
|
|
|
|
|
|
|
|
const fetchCounts = useCallback(async () => {
|
|
|
|
|
if (!tableName || !parentValue || isDesignMode) return;
|
|
|
|
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
try {
|
2026-03-09 15:34:31 +09:00
|
|
|
const res = await apiClient.post(`/table-management/tables/${tableName}/data`, {
|
|
|
|
|
page: 1,
|
|
|
|
|
size: 9999,
|
|
|
|
|
search: relationColumn ? { [relationColumn]: parentValue } : {},
|
2026-03-09 13:15:41 +09:00
|
|
|
});
|
|
|
|
|
|
2026-03-09 15:34:31 +09:00
|
|
|
const responseData = res.data?.data;
|
2026-03-09 18:05:00 +09:00
|
|
|
let rows: any[] = [];
|
|
|
|
|
if (Array.isArray(responseData)) {
|
|
|
|
|
rows = responseData;
|
|
|
|
|
} else if (responseData && typeof responseData === "object") {
|
|
|
|
|
rows = Array.isArray(responseData.data) ? responseData.data :
|
|
|
|
|
Array.isArray(responseData.rows) ? responseData.rows : [];
|
|
|
|
|
}
|
2026-03-09 13:15:41 +09:00
|
|
|
const grouped: Record<string, number> = {};
|
|
|
|
|
|
|
|
|
|
for (const row of rows) {
|
|
|
|
|
const val = row[statusColumn] || "UNKNOWN";
|
|
|
|
|
grouped[val] = (grouped[val] || 0) + 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setCounts(grouped);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error("[v2-status-count] 데이터 조회 실패:", err);
|
|
|
|
|
setCounts({});
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
}, [tableName, statusColumn, relationColumn, parentValue, isDesignMode]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
fetchCounts();
|
|
|
|
|
}, [fetchCounts]);
|
|
|
|
|
|
|
|
|
|
const getColorClasses = (color: string) => {
|
|
|
|
|
if (STATUS_COLOR_MAP[color]) return STATUS_COLOR_MAP[color];
|
|
|
|
|
return { bg: "bg-gray-50", text: "text-gray-600", border: "border-gray-200" };
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getCount = (item: StatusCountItem) => {
|
2026-03-09 15:34:31 +09:00
|
|
|
if (item.value === "__TOTAL__" || item.value === "__ALL__") {
|
2026-03-09 13:15:41 +09:00
|
|
|
return Object.values(counts).reduce((sum, c) => sum + c, 0);
|
|
|
|
|
}
|
|
|
|
|
const values = item.value.split(",").map((v) => v.trim());
|
|
|
|
|
return values.reduce((sum, v) => sum + (counts[v] || 0), 0);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const sizeClasses = {
|
|
|
|
|
sm: { card: "px-3 py-2", number: "text-xl", label: "text-[10px]" },
|
|
|
|
|
md: { card: "px-4 py-3", number: "text-2xl", label: "text-xs" },
|
|
|
|
|
lg: { card: "px-6 py-4", number: "text-3xl", label: "text-sm" },
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const sz = sizeClasses[cardSize] || sizeClasses.md;
|
|
|
|
|
|
|
|
|
|
if (isDesignMode && !parentValue) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex h-full w-full flex-col gap-2 rounded-lg border border-dashed border-gray-300 p-3">
|
|
|
|
|
{title && <div className="text-xs font-medium text-muted-foreground">{title}</div>}
|
|
|
|
|
<div className="flex flex-1 items-center justify-center gap-2">
|
|
|
|
|
{(items.length > 0 ? items : [{ label: "상태1", color: "green" }, { label: "상태2", color: "blue" }, { label: "상태3", color: "orange" }]).map(
|
|
|
|
|
(item: any, i: number) => {
|
|
|
|
|
const colors = getColorClasses(item.color || "gray");
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
key={i}
|
|
|
|
|
className={`flex flex-1 flex-col items-center rounded-lg border ${colors.border} ${colors.bg} ${sz.card}`}
|
|
|
|
|
>
|
|
|
|
|
<span className={`font-bold ${colors.text} ${sz.number}`}>0</span>
|
|
|
|
|
<span className={`${colors.text} ${sz.label}`}>{item.label}</span>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex h-full w-full flex-col gap-2">
|
|
|
|
|
{title && <div className="text-sm font-medium text-foreground">{title}</div>}
|
|
|
|
|
<div className="flex flex-1 items-stretch gap-2">
|
|
|
|
|
{items.map((item, i) => {
|
|
|
|
|
const colors = getColorClasses(item.color);
|
|
|
|
|
const count = getCount(item);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
key={item.value + i}
|
|
|
|
|
className={`flex flex-1 flex-col items-center justify-center rounded-lg border ${colors.border} ${colors.bg} ${sz.card} transition-shadow hover:shadow-sm`}
|
|
|
|
|
>
|
|
|
|
|
<span className={`font-bold ${colors.text} ${sz.number}`}>
|
|
|
|
|
{loading ? "-" : count}
|
|
|
|
|
</span>
|
|
|
|
|
<span className={`mt-0.5 ${colors.text} ${sz.label}`}>{item.label}</span>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const StatusCountWrapper: React.FC<StatusCountComponentProps> = (props) => {
|
|
|
|
|
return <StatusCountComponent {...props} />;
|
|
|
|
|
};
|