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}