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.
This commit is contained in:
parent
4d6783e508
commit
c98b2ccb43
|
|
@ -929,6 +929,42 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
return result;
|
return result;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 프로그레스바 셀 렌더링 (부모 값 대비 자식 값 비율)
|
||||||
|
const renderProgressCell = useCallback(
|
||||||
|
(col: any, item: any, parentData: any) => {
|
||||||
|
const current = Number(item[col.numerator] || 0);
|
||||||
|
const max = Number(parentData?.[col.denominator] || item[col.denominator] || 0);
|
||||||
|
const percentage = max > 0 ? Math.round((current / max) * 100) : 0;
|
||||||
|
const barWidth = Math.min(percentage, 100);
|
||||||
|
const barColor =
|
||||||
|
percentage > 100
|
||||||
|
? "bg-red-600"
|
||||||
|
: percentage >= 90
|
||||||
|
? "bg-red-500"
|
||||||
|
: percentage >= 70
|
||||||
|
? "bg-amber-500"
|
||||||
|
: "bg-emerald-500";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex min-w-[120px] items-center gap-2">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="bg-muted h-2 w-full rounded-full">
|
||||||
|
<div
|
||||||
|
className={`h-2 rounded-full transition-all ${barColor}`}
|
||||||
|
style={{ width: `${barWidth}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="text-muted-foreground mt-0.5 text-[10px]">
|
||||||
|
{current.toLocaleString()} / {max.toLocaleString()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className="shrink-0 text-xs font-medium">{percentage}%</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
// 셀 값 포맷팅 함수 (카테고리 타입 처리 + 날짜/숫자 포맷)
|
// 셀 값 포맷팅 함수 (카테고리 타입 처리 + 날짜/숫자 포맷)
|
||||||
const formatCellValue = useCallback(
|
const formatCellValue = useCallback(
|
||||||
(
|
(
|
||||||
|
|
@ -3950,12 +3986,14 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
>
|
>
|
||||||
{tabSummaryColumns.map((col: any) => (
|
{tabSummaryColumns.map((col: any) => (
|
||||||
<td key={col.name} className="px-3 py-2 text-xs">
|
<td key={col.name} className="px-3 py-2 text-xs">
|
||||||
{formatCellValue(
|
{col.type === "progress"
|
||||||
col.name,
|
? renderProgressCell(col, item, selectedLeftItem)
|
||||||
getEntityJoinValue(item, col.name),
|
: formatCellValue(
|
||||||
rightCategoryMappings,
|
col.name,
|
||||||
col.format,
|
getEntityJoinValue(item, col.name),
|
||||||
)}
|
rightCategoryMappings,
|
||||||
|
col.format,
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
{hasTabActions && (
|
{hasTabActions && (
|
||||||
|
|
@ -4064,12 +4102,14 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
>
|
>
|
||||||
{listSummaryColumns.map((col: any) => (
|
{listSummaryColumns.map((col: any) => (
|
||||||
<td key={col.name} className="px-3 py-2 text-xs">
|
<td key={col.name} className="px-3 py-2 text-xs">
|
||||||
{formatCellValue(
|
{col.type === "progress"
|
||||||
col.name,
|
? renderProgressCell(col, item, selectedLeftItem)
|
||||||
getEntityJoinValue(item, col.name),
|
: formatCellValue(
|
||||||
rightCategoryMappings,
|
col.name,
|
||||||
col.format,
|
getEntityJoinValue(item, col.name),
|
||||||
)}
|
rightCategoryMappings,
|
||||||
|
col.format,
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
{hasTabActions && (
|
{hasTabActions && (
|
||||||
|
|
@ -4486,12 +4526,14 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
className="px-3 py-2 text-xs whitespace-nowrap"
|
className="px-3 py-2 text-xs whitespace-nowrap"
|
||||||
style={{ textAlign: col.align || "left" }}
|
style={{ textAlign: col.align || "left" }}
|
||||||
>
|
>
|
||||||
{formatCellValue(
|
{col.type === "progress"
|
||||||
col.name,
|
? renderProgressCell(col, item, selectedLeftItem)
|
||||||
getEntityJoinValue(item, col.name),
|
: formatCellValue(
|
||||||
rightCategoryMappings,
|
col.name,
|
||||||
col.format,
|
getEntityJoinValue(item, col.name),
|
||||||
)}
|
rightCategoryMappings,
|
||||||
|
col.format,
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
{/* 수정 또는 삭제 버튼이 하나라도 활성화되어 있을 때만 작업 셀 표시 */}
|
{/* 수정 또는 삭제 버튼이 하나라도 활성화되어 있을 때만 작업 셀 표시 */}
|
||||||
|
|
|
||||||
|
|
@ -28,10 +28,10 @@ import { CSS } from "@dnd-kit/utilities";
|
||||||
|
|
||||||
// 드래그 가능한 컬럼 아이템
|
// 드래그 가능한 컬럼 아이템
|
||||||
function SortableColumnRow({
|
function SortableColumnRow({
|
||||||
id, col, index, isNumeric, isEntityJoin, onLabelChange, onWidthChange, onFormatChange, onRemove, onShowInSummaryChange, onShowInDetailChange,
|
id, col, index, isNumeric, isEntityJoin, onLabelChange, onWidthChange, onFormatChange, onRemove, onShowInSummaryChange, onShowInDetailChange, onProgressChange, availableChildColumns, availableParentColumns,
|
||||||
}: {
|
}: {
|
||||||
id: string;
|
id: string;
|
||||||
col: { name: string; label: string; width?: number; format?: any; showInSummary?: boolean; showInDetail?: boolean };
|
col: { name: string; label: string; width?: number; format?: any; showInSummary?: boolean; showInDetail?: boolean; type?: string; numerator?: string; denominator?: string };
|
||||||
index: number;
|
index: number;
|
||||||
isNumeric: boolean;
|
isNumeric: boolean;
|
||||||
isEntityJoin?: boolean;
|
isEntityJoin?: boolean;
|
||||||
|
|
@ -41,6 +41,9 @@ function SortableColumnRow({
|
||||||
onRemove: () => void;
|
onRemove: () => void;
|
||||||
onShowInSummaryChange?: (checked: boolean) => void;
|
onShowInSummaryChange?: (checked: boolean) => void;
|
||||||
onShowInDetailChange?: (checked: boolean) => void;
|
onShowInDetailChange?: (checked: boolean) => void;
|
||||||
|
onProgressChange?: (updates: { numerator?: string; denominator?: string }) => void;
|
||||||
|
availableChildColumns?: Array<{ columnName: string; columnLabel: string }>;
|
||||||
|
availableParentColumns?: Array<{ columnName: string; columnLabel: string }>;
|
||||||
}) {
|
}) {
|
||||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id });
|
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id });
|
||||||
const style = { transform: CSS.Transform.toString(transform), transition };
|
const style = { transform: CSS.Transform.toString(transform), transition };
|
||||||
|
|
@ -53,12 +56,44 @@ function SortableColumnRow({
|
||||||
"flex items-center gap-1.5 rounded-md border bg-card px-2 py-1.5",
|
"flex items-center gap-1.5 rounded-md border bg-card px-2 py-1.5",
|
||||||
isDragging && "z-50 opacity-50 shadow-md",
|
isDragging && "z-50 opacity-50 shadow-md",
|
||||||
isEntityJoin && "border-blue-200 bg-blue-50/30",
|
isEntityJoin && "border-blue-200 bg-blue-50/30",
|
||||||
|
col.type === "progress" && "border-emerald-200 bg-emerald-50/30",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div {...attributes} {...listeners} className="text-muted-foreground hover:text-foreground cursor-grab touch-none">
|
<div {...attributes} {...listeners} className="text-muted-foreground hover:text-foreground cursor-grab touch-none">
|
||||||
<GripVertical className="h-3 w-3" />
|
<GripVertical className="h-3 w-3" />
|
||||||
</div>
|
</div>
|
||||||
{isEntityJoin ? (
|
{col.type === "progress" ? (
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<button className="shrink-0 cursor-pointer rounded bg-emerald-100 px-1 text-[9px] font-medium text-emerald-700 hover:bg-emerald-200" title="클릭하여 설정 변경">BAR</button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-56 space-y-2 p-3" align="start">
|
||||||
|
<p className="text-xs font-medium">프로그레스 설정</p>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-[10px]">현재값 (자식 컬럼)</Label>
|
||||||
|
<Select value={col.numerator || ""} onValueChange={(v) => onProgressChange?.({ numerator: v })}>
|
||||||
|
<SelectTrigger className="h-7 text-[10px]"><SelectValue placeholder="컬럼 선택" /></SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{(availableChildColumns || []).map((c) => (
|
||||||
|
<SelectItem key={c.columnName} value={c.columnName} className="text-xs">{c.columnLabel || c.columnName}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-[10px]">최대값 (부모 컬럼)</Label>
|
||||||
|
<Select value={col.denominator || ""} onValueChange={(v) => onProgressChange?.({ denominator: v })}>
|
||||||
|
<SelectTrigger className="h-7 text-[10px]"><SelectValue placeholder="컬럼 선택" /></SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{(availableParentColumns || []).map((c) => (
|
||||||
|
<SelectItem key={c.columnName} value={c.columnName} className="text-xs">{c.columnLabel || c.columnName}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
) : isEntityJoin ? (
|
||||||
<Link2 className="h-3 w-3 shrink-0 text-blue-500" title="Entity 조인 컬럼" />
|
<Link2 className="h-3 w-3 shrink-0 text-blue-500" title="Entity 조인 컬럼" />
|
||||||
) : (
|
) : (
|
||||||
<span className="text-muted-foreground w-5 shrink-0 text-center text-[10px] font-medium">#{index + 1}</span>
|
<span className="text-muted-foreground w-5 shrink-0 text-center text-[10px] font-medium">#{index + 1}</span>
|
||||||
|
|
@ -656,6 +691,13 @@ const AdditionalTabConfigPanel: React.FC<AdditionalTabConfigPanelProps> = ({
|
||||||
newColumns[index] = { ...newColumns[index], showInDetail: checked };
|
newColumns[index] = { ...newColumns[index], showInDetail: checked };
|
||||||
updateTab({ columns: newColumns });
|
updateTab({ columns: newColumns });
|
||||||
}}
|
}}
|
||||||
|
onProgressChange={(updates) => {
|
||||||
|
const newColumns = [...selectedColumns];
|
||||||
|
newColumns[index] = { ...newColumns[index], ...updates };
|
||||||
|
updateTab({ columns: newColumns });
|
||||||
|
}}
|
||||||
|
availableChildColumns={tabColumns.map((c) => ({ columnName: c.columnName, columnLabel: c.columnLabel || c.columnName }))}
|
||||||
|
availableParentColumns={leftTableColumns.map((c) => ({ columnName: c.columnName, columnLabel: c.columnLabel || c.columnName }))}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
@ -685,6 +727,104 @@ const AdditionalTabConfigPanel: React.FC<AdditionalTabConfigPanelProps> = ({
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 프로그레스 컬럼 추가 */}
|
||||||
|
{tab.tableName && (
|
||||||
|
<div className="border-border/60 my-2 border-t pt-2">
|
||||||
|
<details className="group">
|
||||||
|
<summary className="flex cursor-pointer list-none items-center gap-2 select-none">
|
||||||
|
<ChevronRight className="h-3 w-3 shrink-0 text-emerald-500 transition-transform group-open:rotate-90" />
|
||||||
|
<span className="text-[10px] font-medium text-emerald-600">프로그레스 컬럼 추가</span>
|
||||||
|
</summary>
|
||||||
|
<div className="mt-2 space-y-2 rounded-md border border-emerald-200 bg-emerald-50/50 p-2">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-[10px]">라벨</Label>
|
||||||
|
<Input
|
||||||
|
id={`tab-${tabIndex}-progress-label`}
|
||||||
|
placeholder="예: 샷수 현황"
|
||||||
|
className="h-7 text-xs"
|
||||||
|
defaultValue=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-[10px]">현재값 (자식 컬럼)</Label>
|
||||||
|
<Select
|
||||||
|
onValueChange={(v) => {
|
||||||
|
const el = document.getElementById(`tab-${tabIndex}-progress-numerator`) as HTMLInputElement;
|
||||||
|
if (el) el.value = v;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-7 text-[10px]">
|
||||||
|
<SelectValue placeholder="컬럼 선택" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{tabColumns.map((col) => (
|
||||||
|
<SelectItem key={col.columnName} value={col.columnName} className="text-xs">
|
||||||
|
{col.columnLabel || col.columnName}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<input type="hidden" id={`tab-${tabIndex}-progress-numerator`} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-[10px]">최대값 (부모 컬럼)</Label>
|
||||||
|
<Select
|
||||||
|
onValueChange={(v) => {
|
||||||
|
const el = document.getElementById(`tab-${tabIndex}-progress-denominator`) as HTMLInputElement;
|
||||||
|
if (el) el.value = v;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-7 text-[10px]">
|
||||||
|
<SelectValue placeholder="컬럼 선택" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{leftTableColumns.map((col) => (
|
||||||
|
<SelectItem key={col.columnName} value={col.columnName} className="text-xs">
|
||||||
|
{col.columnLabel || col.columnName}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<input type="hidden" id={`tab-${tabIndex}-progress-denominator`} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 w-full text-xs text-emerald-700 border-emerald-300 hover:bg-emerald-100"
|
||||||
|
onClick={() => {
|
||||||
|
const labelEl = document.getElementById(`tab-${tabIndex}-progress-label`) as HTMLInputElement;
|
||||||
|
const numEl = document.getElementById(`tab-${tabIndex}-progress-numerator`) as HTMLInputElement;
|
||||||
|
const denEl = document.getElementById(`tab-${tabIndex}-progress-denominator`) as HTMLInputElement;
|
||||||
|
const label = labelEl?.value || "프로그레스";
|
||||||
|
const numerator = numEl?.value;
|
||||||
|
const denominator = denEl?.value;
|
||||||
|
if (!numerator || !denominator) return;
|
||||||
|
updateTab({
|
||||||
|
columns: [
|
||||||
|
...selectedColumns,
|
||||||
|
{
|
||||||
|
name: `progress_${numerator}_${denominator}`,
|
||||||
|
label,
|
||||||
|
width: 200,
|
||||||
|
type: "progress",
|
||||||
|
numerator,
|
||||||
|
denominator,
|
||||||
|
} as any,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
if (labelEl) labelEl.value = "";
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
추가
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Entity 조인 컬럼 - 아코디언 (접기/펼치기) */}
|
{/* Entity 조인 컬럼 - 아코디언 (접기/펼치기) */}
|
||||||
{(() => {
|
{(() => {
|
||||||
const joinData = tab.tableName ? entityJoinColumnsMap?.[tab.tableName] : null;
|
const joinData = tab.tableName ? entityJoinColumnsMap?.[tab.tableName] : null;
|
||||||
|
|
|
||||||
|
|
@ -42,9 +42,13 @@ export const StatusCountComponent: React.FC<StatusCountComponentProps> = ({
|
||||||
});
|
});
|
||||||
|
|
||||||
const responseData = res.data?.data;
|
const responseData = res.data?.data;
|
||||||
const rows: any[] = Array.isArray(responseData)
|
let rows: any[] = [];
|
||||||
? responseData
|
if (Array.isArray(responseData)) {
|
||||||
: (responseData?.data || responseData?.rows || []);
|
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> = {};
|
const grouped: Record<string, number> = {};
|
||||||
|
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue