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