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) {