From c98b2ccb4307f974ce14298c406eeed6106664cf Mon Sep 17 00:00:00 2001 From: kjs Date: Mon, 9 Mar 2026 18:05:00 +0900 Subject: [PATCH] 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. --- .../SplitPanelLayoutComponent.tsx | 78 +++++++--- .../SplitPanelLayoutConfigPanel.tsx | 146 +++++++++++++++++- .../v2-status-count/StatusCountComponent.tsx | 10 +- 3 files changed, 210 insertions(+), 24 deletions(-) diff --git a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx index 2bfc7b19..3439c220 100644 --- a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx @@ -929,6 +929,42 @@ export const SplitPanelLayoutComponent: React.FC 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 ( +
+
+
+
+
+
+ {current.toLocaleString()} / {max.toLocaleString()} +
+
+ {percentage}% +
+ ); + }, + [], + ); + // 셀 값 포맷팅 함수 (카테고리 타입 처리 + 날짜/숫자 포맷) const formatCellValue = useCallback( ( @@ -3950,12 +3986,14 @@ export const SplitPanelLayoutComponent: React.FC > {tabSummaryColumns.map((col: any) => ( - {formatCellValue( - col.name, - getEntityJoinValue(item, col.name), - rightCategoryMappings, - col.format, - )} + {col.type === "progress" + ? renderProgressCell(col, item, selectedLeftItem) + : formatCellValue( + col.name, + getEntityJoinValue(item, col.name), + rightCategoryMappings, + col.format, + )} ))} {hasTabActions && ( @@ -4064,12 +4102,14 @@ export const SplitPanelLayoutComponent: React.FC > {listSummaryColumns.map((col: any) => ( - {formatCellValue( - col.name, - getEntityJoinValue(item, col.name), - rightCategoryMappings, - col.format, - )} + {col.type === "progress" + ? renderProgressCell(col, item, selectedLeftItem) + : formatCellValue( + col.name, + getEntityJoinValue(item, col.name), + rightCategoryMappings, + col.format, + )} ))} {hasTabActions && ( @@ -4486,12 +4526,14 @@ export const SplitPanelLayoutComponent: React.FC className="px-3 py-2 text-xs whitespace-nowrap" style={{ textAlign: col.align || "left" }} > - {formatCellValue( - col.name, - getEntityJoinValue(item, col.name), - rightCategoryMappings, - col.format, - )} + {col.type === "progress" + ? renderProgressCell(col, item, selectedLeftItem) + : formatCellValue( + col.name, + getEntityJoinValue(item, col.name), + rightCategoryMappings, + col.format, + )} ))} {/* 수정 또는 삭제 버튼이 하나라도 활성화되어 있을 때만 작업 셀 표시 */} diff --git a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutConfigPanel.tsx b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutConfigPanel.tsx index d77cb88d..c4c699cb 100644 --- a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutConfigPanel.tsx +++ b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutConfigPanel.tsx @@ -28,10 +28,10 @@ import { CSS } from "@dnd-kit/utilities"; // 드래그 가능한 컬럼 아이템 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; - 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; isNumeric: boolean; isEntityJoin?: boolean; @@ -41,6 +41,9 @@ function SortableColumnRow({ onRemove: () => void; onShowInSummaryChange?: (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 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", isDragging && "z-50 opacity-50 shadow-md", isEntityJoin && "border-blue-200 bg-blue-50/30", + col.type === "progress" && "border-emerald-200 bg-emerald-50/30", )} >
- {isEntityJoin ? ( + {col.type === "progress" ? ( + + + + + +

프로그레스 설정

+
+ + +
+
+ + +
+
+
+ ) : isEntityJoin ? ( ) : ( #{index + 1} @@ -656,6 +691,13 @@ const AdditionalTabConfigPanel: React.FC = ({ newColumns[index] = { ...newColumns[index], showInDetail: checked }; 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 = ({ ))}
+ {/* 프로그레스 컬럼 추가 */} + {tab.tableName && ( +
+
+ + + 프로그레스 컬럼 추가 + +
+
+ + +
+
+
+ + + +
+
+ + + +
+
+ +
+
+
+ )} + {/* Entity 조인 컬럼 - 아코디언 (접기/펼치기) */} {(() => { const joinData = tab.tableName ? entityJoinColumnsMap?.[tab.tableName] : null; diff --git a/frontend/lib/registry/components/v2-status-count/StatusCountComponent.tsx b/frontend/lib/registry/components/v2-status-count/StatusCountComponent.tsx index d0c388e3..048ce076 100644 --- a/frontend/lib/registry/components/v2-status-count/StatusCountComponent.tsx +++ b/frontend/lib/registry/components/v2-status-count/StatusCountComponent.tsx @@ -42,9 +42,13 @@ export const StatusCountComponent: React.FC = ({ }); const responseData = res.data?.data; - const rows: any[] = Array.isArray(responseData) - ? responseData - : (responseData?.data || responseData?.rows || []); + 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 = {}; for (const row of rows) {