From 13b2ebaf1f4c26dfcd0f25718d17df254475ee35 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Tue, 17 Mar 2026 22:02:52 +0900 Subject: [PATCH] Refactor ColumnDetailPanel and AppLayout for improved loading state handling and UI consistency. Enhance TabBar and TableListComponent styles for better user experience. Update V2SplitPanelLayoutConfigPanel to manage button visibility based on configuration. Introduce filter chips in TableListComponent for better filter management. --- .../screen/RealtimePreviewDynamic.tsx | 21 +-- .../ButtonPrimaryComponent.tsx | 2 +- .../SplitPanelLayoutComponent.tsx | 166 +++++++++++------- .../v2-table-list/SingleTableWithSticky.tsx | 8 +- .../v2-table-list/TableListComponent.tsx | 19 +- 5 files changed, 124 insertions(+), 92 deletions(-) diff --git a/frontend/components/screen/RealtimePreviewDynamic.tsx b/frontend/components/screen/RealtimePreviewDynamic.tsx index d23337b5..14314a61 100644 --- a/frontend/components/screen/RealtimePreviewDynamic.tsx +++ b/frontend/components/screen/RealtimePreviewDynamic.tsx @@ -359,20 +359,10 @@ const RealtimePreviewDynamicComponent: React.FC = ({ return `${actualHeight}px`; } - // 런타임 모드에서 컴포넌트 타입별 높이 처리 + // 런타임 모드: ResponsiveGridRenderer가 ratio 기반으로 래퍼 높이를 설정하므로, + // 안쪽 컴포넌트는 "100%"로 래퍼를 채워야 비율이 정확하게 맞음 if (!isDesignMode) { const compType = (component as any).componentType || component.componentConfig?.type || ""; - // 테이블: 부모 flex 컨테이너가 높이 관리 (flex: 1) - const flexGrowTypes = [ - "table-list", "v2-table-list", - "split-panel-layout", "split-panel-layout2", - "v2-split-panel-layout", "screen-split-panel", - "v2-tab-container", "tab-container", - "tabs-widget", "v2-tabs-widget", - ]; - if (flexGrowTypes.some(t => compType === t)) { - return "100%"; - } const autoHeightTypes = [ "table-search-widget", "v2-table-search-widget", "flow-widget", @@ -380,9 +370,11 @@ const RealtimePreviewDynamicComponent: React.FC = ({ if (autoHeightTypes.some(t => compType === t || compType.includes(t))) { return "auto"; } + // 나머지 모든 타입: 래퍼의 비율 스케일링을 따르도록 100% + return "100%"; } - // 1순위: size.height가 있으면 우선 사용 + // 디자인 모드: 고정 픽셀 사용 (캔버스 내 절대 좌표 배치) if (size?.height && size.height > 0) { if (component.componentConfig?.type === "table-list") { return `${Math.max(size.height, 200)}px`; @@ -390,17 +382,14 @@ const RealtimePreviewDynamicComponent: React.FC = ({ return `${size.height}px`; } - // 2순위: componentStyle.height (컴포넌트 정의에서 온 기본 스타일) if (componentStyle?.height) { return typeof componentStyle.height === "number" ? `${componentStyle.height}px` : componentStyle.height; } - // 3순위: 기본값 if (component.componentConfig?.type === "table-list") { return "200px"; } - // 기본 높이 return "10px"; }; diff --git a/frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx b/frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx index 26a5d7c4..dc3dccc0 100644 --- a/frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx +++ b/frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx @@ -1410,7 +1410,7 @@ export const ButtonPrimaryComponent: React.FC = ({ const buttonElementStyle: React.CSSProperties = { width: buttonWidth, height: buttonHeight, - minHeight: "32px", // 🔧 최소 높이를 32px로 줄임 + minHeight: undefined, // 비율 스케일링 시 래퍼 높이를 정확히 따르도록 제거 // 커스텀 테두리 스타일 (StyleEditor 설정 우선, shorthand 사용 안 함) borderWidth: style?.borderWidth || "0", borderStyle: (style?.borderStyle as React.CSSProperties["borderStyle"]) || (style?.borderWidth ? "solid" : "none"), 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 0c585587..40f00e1a 100644 --- a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx @@ -22,6 +22,7 @@ import { FileSpreadsheet, List, PanelRight, + GripVertical, } from "lucide-react"; import { dataApi } from "@/lib/api/data"; import { entityJoinApi } from "@/lib/api/entityJoin"; @@ -313,10 +314,11 @@ export const SplitPanelLayoutComponent: React.FC const [rightFilters, setRightFilters] = useState([]); const [rightGrouping, setRightGrouping] = useState([]); const [rightColumnVisibility, setRightColumnVisibility] = useState([]); - // 우측 패널 컬럼 헤더 드래그 (디자인 모드에서 순서 변경) + // 우측 패널 컬럼 헤더 드래그 (디자인 + 런타임 순서 변경) const [rightDraggedColumnIndex, setRightDraggedColumnIndex] = useState(null); const [rightDropTargetColumnIndex, setRightDropTargetColumnIndex] = useState(null); const [rightDragSource, setRightDragSource] = useState<"main" | number | null>(null); + const [runtimeColumnOrder, setRuntimeColumnOrder] = useState>({}); // 데이터 상태 const [leftData, setLeftData] = useState([]); @@ -2544,55 +2546,67 @@ export const SplitPanelLayoutComponent: React.FC handleRightColumnDragEnd(); return; } - if (!onUpdateComponent) { - handleRightColumnDragEnd(); - return; - } - const rightPanel = componentConfig.rightPanel || {}; - if (source === "main") { - const allColumns = rightPanel.columns || []; - const visibleColumns = allColumns.filter((c: any) => c.showInSummary !== false); - const hiddenColumns = allColumns.filter((c: any) => c.showInSummary === false); - if (fromIdx < 0 || fromIdx >= visibleColumns.length || targetIndex < 0 || targetIndex >= visibleColumns.length) { - handleRightColumnDragEnd(); - return; + + if (onUpdateComponent) { + // 디자인 모드: config에 영구 저장 + const rightPanel = componentConfig.rightPanel || {}; + if (source === "main") { + const allColumns = rightPanel.columns || []; + const visibleColumns = allColumns.filter((c: any) => c.showInSummary !== false); + const hiddenColumns = allColumns.filter((c: any) => c.showInSummary === false); + if (fromIdx < 0 || fromIdx >= visibleColumns.length || targetIndex < 0 || targetIndex >= visibleColumns.length) { + handleRightColumnDragEnd(); + return; + } + const reordered = [...visibleColumns]; + const [removed] = reordered.splice(fromIdx, 1); + reordered.splice(targetIndex, 0, removed); + const columns = [...reordered, ...hiddenColumns]; + onUpdateComponent({ + ...component, + componentConfig: { + ...componentConfig, + rightPanel: { ...rightPanel, columns }, + }, + }); + } else { + const tabs = [...(rightPanel.additionalTabs || [])]; + const tabConfig = tabs[source]; + if (!tabConfig || !Array.isArray(tabConfig.columns)) { + handleRightColumnDragEnd(); + return; + } + const allTabCols = tabConfig.columns; + const visibleTabCols = allTabCols.filter((c: any) => c.showInSummary !== false); + const hiddenTabCols = allTabCols.filter((c: any) => c.showInSummary === false); + if (fromIdx < 0 || fromIdx >= visibleTabCols.length || targetIndex < 0 || targetIndex >= visibleTabCols.length) { + handleRightColumnDragEnd(); + return; + } + const reordered = [...visibleTabCols]; + const [removed] = reordered.splice(fromIdx, 1); + reordered.splice(targetIndex, 0, removed); + const columns = [...reordered, ...hiddenTabCols]; + const newTabs = tabs.map((t, i) => (i === source ? { ...t, columns } : t)); + onUpdateComponent({ + ...component, + componentConfig: { + ...componentConfig, + rightPanel: { ...rightPanel, additionalTabs: newTabs }, + }, + }); } - const reordered = [...visibleColumns]; - const [removed] = reordered.splice(fromIdx, 1); - reordered.splice(targetIndex, 0, removed); - const columns = [...reordered, ...hiddenColumns]; - onUpdateComponent({ - ...component, - componentConfig: { - ...componentConfig, - rightPanel: { ...rightPanel, columns }, - }, - }); } else { - const tabs = [...(rightPanel.additionalTabs || [])]; - const tabConfig = tabs[source]; - if (!tabConfig || !Array.isArray(tabConfig.columns)) { - handleRightColumnDragEnd(); - return; - } - const allTabCols = tabConfig.columns; - const visibleTabCols = allTabCols.filter((c: any) => c.showInSummary !== false); - const hiddenTabCols = allTabCols.filter((c: any) => c.showInSummary === false); - if (fromIdx < 0 || fromIdx >= visibleTabCols.length || targetIndex < 0 || targetIndex >= visibleTabCols.length) { - handleRightColumnDragEnd(); - return; - } - const reordered = [...visibleTabCols]; - const [removed] = reordered.splice(fromIdx, 1); - reordered.splice(targetIndex, 0, removed); - const columns = [...reordered, ...hiddenTabCols]; - const newTabs = tabs.map((t, i) => (i === source ? { ...t, columns } : t)); - onUpdateComponent({ - ...component, - componentConfig: { - ...componentConfig, - rightPanel: { ...rightPanel, additionalTabs: newTabs }, - }, + // 런타임 모드: 로컬 상태로 순서 변경 + const key = String(source); + setRuntimeColumnOrder((prev) => { + const existing = prev[key]; + const maxLen = 100; + const order = existing || Array.from({ length: maxLen }, (_, i) => i); + const reordered = [...order]; + const [removed] = reordered.splice(fromIdx, 1); + reordered.splice(targetIndex, 0, removed); + return { ...prev, [key]: reordered }; }); } handleRightColumnDragEnd(); @@ -2604,9 +2618,29 @@ export const SplitPanelLayoutComponent: React.FC component, onUpdateComponent, handleRightColumnDragEnd, + setRuntimeColumnOrder, ], ); + // 런타임 컬럼 순서 적용 헬퍼 + const applyRuntimeOrder = useCallback( + (columns: T[], source: "main" | number): T[] => { + const key = String(source); + const order = runtimeColumnOrder[key]; + if (!order) return columns; + const result: T[] = []; + for (const idx of order) { + if (idx < columns.length) result.push(columns[idx]); + } + // order에 없는 나머지 컬럼 추가 + for (let i = 0; i < columns.length; i++) { + if (!order.includes(i)) result.push(columns[i]); + } + return result.length > 0 ? result : columns; + }, + [runtimeColumnOrder], + ); + // 수정 모달 저장 const handleEditModalSave = useCallback(async () => { const tableName = @@ -3946,11 +3980,10 @@ export const SplitPanelLayoutComponent: React.FC {resizable && (
-
-
+
)} @@ -4107,7 +4140,7 @@ export const SplitPanelLayoutComponent: React.FC // showInSummary가 false가 아닌 것만 메인 테이블에 표시 const tabSummaryColumns = tabColumns.filter((col: any) => col.showInSummary !== false); const tabIndex = activeTabIndex - 1; - const canDragTabColumns = isDesignMode && tabSummaryColumns.length > 0 && !!onUpdateComponent; + const canDragTabColumns = tabSummaryColumns.length > 0; return (
@@ -4120,7 +4153,7 @@ export const SplitPanelLayoutComponent: React.FC ); @@ -4158,7 +4192,7 @@ export const SplitPanelLayoutComponent: React.FC toggleRightItemExpansion(`tab_${activeTabIndex}_${tabItemId}`)} > @@ -4243,7 +4277,7 @@ export const SplitPanelLayoutComponent: React.FC // showInSummary가 false가 아닌 것만 메인 테이블에 표시 const listSummaryColumns = tabColumns.filter((col: any) => col.showInSummary !== false); const listTabIndex = activeTabIndex - 1; - const canDragListTabColumns = isDesignMode && listSummaryColumns.length > 0 && !!onUpdateComponent; + const canDragListTabColumns = listSummaryColumns.length > 0; return (
onDragEnd={handleRightColumnDragEnd} onDrop={(e) => canDragTabColumns && handleRightColumnDrop(e, idx, tabIndex)} > + {canDragTabColumns && } {col.label || col.name}
@@ -4256,7 +4290,7 @@ export const SplitPanelLayoutComponent: React.FC ); @@ -4293,7 +4328,7 @@ export const SplitPanelLayoutComponent: React.FC toggleRightItemExpansion(`tab_${activeTabIndex}_${tabItemId}`)} > @@ -4646,6 +4681,14 @@ export const SplitPanelLayoutComponent: React.FC })); } + // 런타임 컬럼 순서 적용 + if (!isDesignMode && runtimeColumnOrder["main"]) { + const keyColCount = columnsToShow.filter((c: any) => c._isKeyColumn).length; + const keyCols = columnsToShow.slice(0, keyColCount); + const dataCols = columnsToShow.slice(keyColCount); + columnsToShow = [...keyCols, ...applyRuntimeOrder(dataCols, "main")]; + } + // 컬럼 너비 합계 계산 (작업 컬럼 제외, 100% 초과 시 스크롤) const rightTotalColWidth = columnsToShow.reduce((sum, col) => { const w = col.width && col.width <= 100 ? col.width : 0; @@ -4653,7 +4696,7 @@ export const SplitPanelLayoutComponent: React.FC }, 0); const rightConfigColumnStart = columnsToShow.filter((c: any) => c._isKeyColumn).length; - const canDragRightColumns = isDesignMode && displayColumns.length > 0 && !!onUpdateComponent; + const canDragRightColumns = displayColumns.length > 0; return (
@@ -4670,7 +4713,7 @@ export const SplitPanelLayoutComponent: React.FC
); @@ -4707,7 +4751,7 @@ export const SplitPanelLayoutComponent: React.FC const rightDeleteVisible = (componentConfig.rightPanel?.showDelete ?? componentConfig.rightPanel?.deleteButton?.enabled) !== false; return ( - + {columnsToShow.map((col, colIdx) => ( toggleRightItemExpansion(itemId)} > diff --git a/frontend/lib/registry/components/v2-table-list/SingleTableWithSticky.tsx b/frontend/lib/registry/components/v2-table-list/SingleTableWithSticky.tsx index 00daa8eb..c264dec2 100644 --- a/frontend/lib/registry/components/v2-table-list/SingleTableWithSticky.tsx +++ b/frontend/lib/registry/components/v2-table-list/SingleTableWithSticky.tsx @@ -110,7 +110,7 @@ export const SingleTableWithSticky: React.FC = ({ > {actualColumns.map((column, colIndex) => { @@ -136,7 +136,7 @@ export const SingleTableWithSticky: React.FC = ({ ? "h-9 border-0 px-3 py-1.5 text-center align-middle sm:px-4 sm:py-2" : "text-muted-foreground hover:text-foreground h-9 cursor-pointer border-0 px-3 py-1.5 text-left align-middle text-[10px] font-bold uppercase tracking-[0.04em] whitespace-nowrap transition-all duration-200 select-none sm:px-4 sm:py-2 sm:text-xs", `text-${column.align}`, - column.sortable && "hover:bg-muted/70", + column.sortable && "hover:bg-muted/50", // 고정 컬럼 스타일 column.fixed === "left" && "border-border bg-background sticky z-40 border-r shadow-sm", column.fixed === "right" && "border-border bg-background sticky z-40 border-l shadow-sm", @@ -151,7 +151,7 @@ export const SingleTableWithSticky: React.FC = ({ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", // 텍스트 줄바꿈 방지 - backgroundColor: "hsl(var(--muted) / 0.8)", + backgroundColor: "hsl(var(--muted) / 0.4)", // sticky 위치 설정 ...(column.fixed === "left" && { left: leftFixedWidth }), ...(column.fixed === "right" && { right: rightFixedWidth }), @@ -230,7 +230,7 @@ export const SingleTableWithSticky: React.FC = ({ key={`row-${index}`} className={cn( "cursor-pointer border-b border-border/50 transition-[background] duration-75", - index % 2 === 0 ? "bg-background" : "bg-muted/70", + index % 2 === 0 ? "bg-background" : "bg-muted/20", tableConfig.tableStyle?.hoverEffect !== false && "hover:bg-accent", )} onClick={(e) => handleRowClick?.(row, index, e)} diff --git a/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx b/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx index f9e39810..7331c7bd 100644 --- a/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx @@ -5819,8 +5819,8 @@ export const TableListComponent: React.FC = ({ {/* 🆕 Multi-Level Headers (Column Bands) */} {columnBandsInfo?.hasBands && ( {visibleColumns.map((column, colIdx) => { // 이 컬럼이 속한 band 찾기 @@ -5863,7 +5863,7 @@ export const TableListComponent: React.FC = ({ {visibleColumns.map((column, columnIndex) => { @@ -5895,7 +5895,7 @@ export const TableListComponent: React.FC = ({ column.columnName === "__checkbox__" ? "px-0 py-1" : "px-3 py-2", column.sortable !== false && column.columnName !== "__checkbox__" && - "hover:text-foreground hover:bg-muted/70 cursor-pointer transition-colors", + "hover:text-foreground hover:bg-muted/50 cursor-pointer transition-colors", sortColumn === column.columnName && "!text-primary", isFrozen && "sticky z-40 shadow-[2px_0_4px_rgba(0,0,0,0.1)]", // 🆕 Column Reordering 스타일 @@ -5916,7 +5916,7 @@ export const TableListComponent: React.FC = ({ minWidth: column.columnName === "__checkbox__" ? "48px" : undefined, maxWidth: column.columnName === "__checkbox__" ? "48px" : undefined, userSelect: "none", - backgroundColor: "hsl(var(--muted) / 0.8)", + backgroundColor: "hsl(var(--muted) / 0.4)", ...(isFrozen && { left: `${leftPosition}px` }), }} // 🆕 Column Reordering 이벤트 @@ -6167,7 +6167,7 @@ export const TableListComponent: React.FC = ({ key={index} className={cn( "hover:bg-accent cursor-pointer border-b border-border/50 transition-[background] duration-75", - index % 2 === 0 ? "bg-background" : "bg-muted/70", + index % 2 === 0 ? "bg-background" : "bg-muted/20", )} onClick={(e) => handleRowClick(row, index, e)} > @@ -6306,9 +6306,8 @@ export const TableListComponent: React.FC = ({ key={index} className={cn( "hover:bg-accent cursor-pointer border-b border-border/50 transition-[background] duration-75", - index % 2 === 0 ? "bg-background" : "bg-muted/70", - isRowSelected && "!bg-primary/15 hover:!bg-primary/20", - isRowSelected && "[&_td]:!border-b-primary/30", + index % 2 === 0 ? "bg-background" : "bg-muted/20", + isRowSelected && "!bg-primary/10 hover:!bg-primary/15", isRowFocused && "ring-primary/50 ring-1 ring-inset", isDragEnabled && "cursor-grab active:cursor-grabbing", isDragging && "bg-muted opacity-50", @@ -6566,7 +6565,7 @@ export const TableListComponent: React.FC = ({ : undefined, ...(isFrozen && { left: `${leftPosition}px`, - backgroundColor: "hsl(var(--muted) / 0.8)", + backgroundColor: "hsl(var(--muted) / 0.4)", }), }} >
onDragEnd={handleRightColumnDragEnd} onDrop={(e) => canDragListTabColumns && handleRightColumnDrop(e, idx, listTabIndex)} > + {canDragListTabColumns && } {col.label || col.name}
onDragEnd={handleRightColumnDragEnd} onDrop={(e) => isDraggable && handleRightColumnDrop(e, configColIndex, "main")} > + {isDraggable && } {col.label}