Files
vexplor/frontend/lib/registry/components/v2-status-count/StatusCountComponent.tsx
kjs c98b2ccb43 feat: Add progress bar functionality to SplitPanelLayoutComponent and configuration options
- Implemented a new progress bar rendering function in the SplitPanelLayoutComponent to visually represent the ratio of child to parent values.
- Enhanced the SortableColumnRow component to support progress column configuration, allowing users to set current and maximum values through a popover interface.
- Updated the AdditionalTabConfigPanel to include options for adding progress columns, improving user experience in managing data visualization.

These changes significantly enhance the functionality and usability of the split panel layout by providing visual progress indicators and configuration options for users.
2026-03-09 18:05:00 +09:00

145 lines
5.0 KiB
TypeScript

"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 {
const res = await apiClient.post(`/table-management/tables/${tableName}/data`, {
page: 1,
size: 9999,
search: relationColumn ? { [relationColumn]: parentValue } : {},
});
const responseData = res.data?.data;
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 : [];
}
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) => {
if (item.value === "__TOTAL__" || item.value === "__ALL__") {
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} />;
};