diff --git a/frontend/components/screen/RealtimePreviewDynamic.tsx b/frontend/components/screen/RealtimePreviewDynamic.tsx index d23337b5..ce76857b 100644 --- a/frontend/components/screen/RealtimePreviewDynamic.tsx +++ b/frontend/components/screen/RealtimePreviewDynamic.tsx @@ -362,15 +362,15 @@ const RealtimePreviewDynamicComponent: React.FC = ({ // 런타임 모드에서 컴포넌트 타입별 높이 처리 if (!isDesignMode) { const compType = (component as any).componentType || component.componentConfig?.type || ""; - // 테이블: 부모 flex 컨테이너가 높이 관리 (flex: 1) - const flexGrowTypes = [ + // 레이아웃 계열: 부모 래퍼를 꽉 채움 (ResponsiveGridRenderer가 % 높이 관리) + const fillParentTypes = [ "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)) { + if (fillParentTypes.some(t => compType === t)) { return "100%"; } const autoHeightTypes = [ diff --git a/frontend/components/screen/ResponsiveGridRenderer.tsx b/frontend/components/screen/ResponsiveGridRenderer.tsx index 1322ee99..47a2cd52 100644 --- a/frontend/components/screen/ResponsiveGridRenderer.tsx +++ b/frontend/components/screen/ResponsiveGridRenderer.tsx @@ -23,8 +23,9 @@ function getComponentTypeId(component: ComponentData): string { } /** - * 디자이너 절대좌표를 캔버스 대비 비율로 변환하여 렌더링. - * 화면이 줄어들면 비율에 맞게 축소, 늘어나면 확대. + * 디자이너 절대좌표를 캔버스 대비 비율(%)로 변환하여 렌더링. + * 가로: 컨테이너 너비 대비 % → 반응형 스케일 + * 세로: 컨테이너 높이 대비 % → 뷰포트에 맞게 자동 조절 */ function ProportionalRenderer({ components, @@ -47,19 +48,12 @@ function ProportionalRenderer({ }, []); const topLevel = components.filter((c) => !c.parentId); - const ratio = containerW > 0 ? containerW / canvasWidth : 1; - - const maxBottom = topLevel.reduce((max, c) => { - const bottom = c.position.y + (c.size?.height || 40); - return Math.max(max, bottom); - }, 0); return (
0 ? `${maxBottom * ratio}px` : "200px" }} + className="bg-background relative h-full w-full overflow-hidden" > {containerW > 0 && topLevel.map((component) => { @@ -72,9 +66,9 @@ function ProportionalRenderer({ style={{ position: "absolute", left: `${(component.position.x / canvasWidth) * 100}%`, - top: `${component.position.y * ratio}px`, + top: `${(component.position.y / canvasHeight) * 100}%`, width: `${((component.size?.width || 100) / canvasWidth) * 100}%`, - height: `${(component.size?.height || 40) * ratio}px`, + height: `${((component.size?.height || 40) / canvasHeight) * 100}%`, zIndex: component.position.z || 1, }} > diff --git a/frontend/lib/registry/components/v2-table-list/SingleTableWithSticky.tsx b/frontend/lib/registry/components/v2-table-list/SingleTableWithSticky.tsx index c264dec2..3a7b4dad 100644 --- a/frontend/lib/registry/components/v2-table-list/SingleTableWithSticky.tsx +++ b/frontend/lib/registry/components/v2-table-list/SingleTableWithSticky.tsx @@ -144,28 +144,33 @@ export const SingleTableWithSticky: React.FC = ({ isDesignMode && column.hidden && "bg-muted/50 opacity-40", )} style={{ - width: getColumnWidth(column), - minWidth: "100px", // 최소 너비 보장 - maxWidth: "300px", // 최대 너비 제한 + width: column.columnName === "__checkbox__" ? 48 : getColumnWidth(column), + minWidth: column.columnName === "__checkbox__" ? "48px" : "100px", + maxWidth: column.columnName === "__checkbox__" ? "48px" : "300px", boxSizing: "border-box", overflow: "hidden", textOverflow: "ellipsis", - whiteSpace: "nowrap", // 텍스트 줄바꿈 방지 + whiteSpace: "nowrap", backgroundColor: "hsl(var(--muted) / 0.4)", - // sticky 위치 설정 ...(column.fixed === "left" && { left: leftFixedWidth }), ...(column.fixed === "right" && { right: rightFixedWidth }), }} onClick={() => column.sortable && sortHandler(column.columnName)} > -
+
{column.columnName === "__checkbox__" ? ( checkboxConfig.selectAll && ( ) ) : ( @@ -327,26 +332,22 @@ export const SingleTableWithSticky: React.FC = ({ key={`cell-${column.columnName}`} id={isCurrentSearchResult ? "current-search-result" : undefined} className={cn( - "text-foreground h-10 px-3 py-[7px] align-middle text-[11px] transition-colors", - // 이미지 셀은 overflow/ellipsis 제외 (이미지 잘림 방지) + "text-foreground h-10 align-middle text-[11px] transition-colors", + column.columnName === "__checkbox__" ? "px-0 py-[7px] text-center" : "px-3 py-[7px]", !isReactElement && "whitespace-nowrap", - `text-${column.align}`, - // 고정 컬럼 스타일 + column.columnName !== "__checkbox__" && `text-${column.align}`, column.fixed === "left" && "border-border bg-background/90 sticky z-10 border-r backdrop-blur-sm", column.fixed === "right" && "border-border bg-background/90 sticky z-10 border-l backdrop-blur-sm", - // 편집 가능 셀 스타일 onCellDoubleClick && column.columnName !== "__checkbox__" && "cursor-text", )} style={{ - width: getColumnWidth(column), - minWidth: "100px", // 최소 너비 보장 - maxWidth: "300px", // 최대 너비 제한 + width: column.columnName === "__checkbox__" ? 48 : getColumnWidth(column), + minWidth: column.columnName === "__checkbox__" ? "48px" : "100px", + maxWidth: column.columnName === "__checkbox__" ? "48px" : "300px", boxSizing: "border-box", - // 이미지 셀은 overflow 허용 ...(isReactElement ? {} : { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }), - // sticky 위치 설정 ...(column.fixed === "left" && { left: leftFixedWidth }), ...(column.fixed === "right" && { right: rightFixedWidth }), }} diff --git a/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx b/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx index 7331c7bd..3f41b185 100644 --- a/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx @@ -878,10 +878,7 @@ export const TableListComponent: React.FC = ({ const [isTableOptionsOpen, setIsTableOptionsOpen] = useState(false); const [showGridLines, setShowGridLines] = useState(true); const [viewMode, setViewMode] = useState<"table" | "card" | "grouped-card">("table"); - // 체크박스 컬럼은 항상 기본 틀고정 - const [frozenColumns, setFrozenColumns] = useState( - (tableConfig.checkbox?.enabled ?? true) ? ["__checkbox__"] : [], - ); + const [frozenColumns, setFrozenColumns] = useState([]); const [frozenColumnCount, setFrozenColumnCount] = useState(0); // 🆕 Search Panel (통합 검색) 관련 상태 @@ -1173,14 +1170,10 @@ export const TableListComponent: React.FC = ({ frozenColumnCount, // 현재 틀고정 컬럼 수 onFrozenColumnCountChange: (count: number) => { setFrozenColumnCount(count); - // 체크박스 컬럼은 항상 틀고정에 포함 - const checkboxColumn = (tableConfig.checkbox?.enabled ?? true) ? ["__checkbox__"] : []; - // 표시 가능한 컬럼 중 처음 N개를 틀고정 컬럼으로 설정 const visibleCols = columnsToRegister .filter((col) => col.visible !== false) .map((col) => col.columnName || col.field); - const newFrozenColumns = [...checkboxColumn, ...visibleCols.slice(0, count)]; - setFrozenColumns(newFrozenColumns); + setFrozenColumns(visibleCols.slice(0, count)); }, // 탭 관련 정보 (탭 내부의 테이블인 경우) parentTabId, @@ -3080,11 +3073,7 @@ export const TableListComponent: React.FC = ({ if (state.groupByColumns) setGroupByColumns(state.groupByColumns); if (state.frozenColumns) { // 체크박스 컬럼이 항상 포함되도록 보장 - const checkboxColumn = (tableConfig.checkbox?.enabled ?? true) ? "__checkbox__" : null; - const restoredFrozenColumns = - checkboxColumn && !state.frozenColumns.includes(checkboxColumn) - ? [checkboxColumn, ...state.frozenColumns] - : state.frozenColumns; + const restoredFrozenColumns = (state.frozenColumns || []).filter((col: string) => col !== "__checkbox__"); setFrozenColumns(restoredFrozenColumns); } if (state.frozenColumnCount !== undefined) setFrozenColumnCount(state.frozenColumnCount); // 틀고정 컬럼 수 복원 @@ -4233,7 +4222,19 @@ export const TableListComponent: React.FC = ({ if (!tableConfig.checkbox?.selectAll) return null; if (tableConfig.checkbox?.multiple === false) return null; - return ; + return ( + + ); }; const renderCheckboxCell = (row: any, index: number) => { @@ -4245,6 +4246,12 @@ export const TableListComponent: React.FC = ({ checked={isChecked} onCheckedChange={(checked) => handleRowSelection(rowKey, checked as boolean)} aria-label={`행 ${index + 1} 선택`} + style={{ + width: 16, + height: 16, + borderWidth: 1.5, + borderColor: isChecked ? "hsl(var(--primary))" : "hsl(var(--muted-foreground) / 0.5)", + }} /> ); }; @@ -6201,7 +6208,7 @@ export const TableListComponent: React.FC = ({ "text-foreground text-[11px] font-normal", inputType !== "image" && "overflow-hidden text-ellipsis whitespace-nowrap max-w-[170px]", column.columnName === "__checkbox__" - ? "px-0 py-[7px]" + ? "px-0 py-[7px] text-center" : "px-3 py-[7px]", isFrozen && "sticky z-20 shadow-[2px_0_4px_rgba(0,0,0,0.08)]", (inputType === "code" || inputType === "category") && "font-mono text-[10px] text-primary font-medium", @@ -6370,7 +6377,7 @@ export const TableListComponent: React.FC = ({ className={cn( "text-foreground text-[11px] font-normal", inputType !== "image" && "overflow-hidden text-ellipsis whitespace-nowrap max-w-[170px]", - column.columnName === "__checkbox__" ? "px-0 py-[7px]" : "px-3 py-[7px]", + column.columnName === "__checkbox__" ? "px-0 py-[7px] text-center" : "px-3 py-[7px]", isFrozen && "sticky z-20 shadow-[2px_0_4px_rgba(0,0,0,0.08)]", isCellFocused && !editingCell && "ring-primary bg-primary/5 ring-2 ring-inset", editingCell?.rowIndex === index && editingCell?.colIndex === colIndex && "p-0",