From c63eaf8434c093dec52a355dc6438efcc91b81f3 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Tue, 17 Mar 2026 22:49:42 +0900 Subject: [PATCH] 123123 --- frontend/components/admin/CompanySwitcher.tsx | 2 + .../screen/RealtimePreviewDynamic.tsx | 25 ++-- .../screen/ResponsiveGridRenderer.tsx | 64 +++++----- .../ButtonPrimaryComponent.tsx | 2 +- .../SplitPanelLayoutComponent.tsx | 110 +++++++++++++++--- 5 files changed, 142 insertions(+), 61 deletions(-) diff --git a/frontend/components/admin/CompanySwitcher.tsx b/frontend/components/admin/CompanySwitcher.tsx index c37d82a5..23445780 100644 --- a/frontend/components/admin/CompanySwitcher.tsx +++ b/frontend/components/admin/CompanySwitcher.tsx @@ -174,6 +174,8 @@ export function CompanySwitcher({ onClose, isOpen = false }: CompanySwitcherProp ? "bg-accent/50 font-semibold" : "" }`} + role="button" + aria-label={`${company.company_name} ${company.company_code}`} onClick={() => handleCompanySwitch(company.company_code)} >
diff --git a/frontend/components/screen/RealtimePreviewDynamic.tsx b/frontend/components/screen/RealtimePreviewDynamic.tsx index 6330b8d4..d23337b5 100644 --- a/frontend/components/screen/RealtimePreviewDynamic.tsx +++ b/frontend/components/screen/RealtimePreviewDynamic.tsx @@ -359,10 +359,20 @@ 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", @@ -370,11 +380,9 @@ 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`; @@ -382,14 +390,17 @@ 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"; }; @@ -592,10 +603,10 @@ const RealtimePreviewDynamicComponent: React.FC = ({ isResizing ? "none" : isOnSplitPanel ? (isDraggingSplitPanel ? "none" : "left 0.15s ease-out, width 0.15s ease-out") : undefined, } : { - // 런타임 모드: CSS scale 기반 - 캔버스 픽셀 크기 그대로 사용, 부모가 scale()로 축소 + // 런타임 모드: 부모(ResponsiveGridRenderer)가 위치/너비 관리 ...safeComponentStyle, width: "100%", - height: "100%", + height: displayHeight, position: "relative" as const, }; diff --git a/frontend/components/screen/ResponsiveGridRenderer.tsx b/frontend/components/screen/ResponsiveGridRenderer.tsx index d90a0667..1322ee99 100644 --- a/frontend/components/screen/ResponsiveGridRenderer.tsx +++ b/frontend/components/screen/ResponsiveGridRenderer.tsx @@ -23,9 +23,8 @@ function getComponentTypeId(component: ComponentData): string { } /** - * CSS transform scale 기반 렌더링. - * 디자이너와 동일하게 캔버스 해상도(px)로 레이아웃 후 CSS scale로 축소/확대. - * 텍스트, 패딩, 버튼 등 모든 요소가 균일하게 스케일링되어 WYSIWYG 보장. + * 디자이너 절대좌표를 캔버스 대비 비율로 변환하여 렌더링. + * 화면이 줄어들면 비율에 맞게 축소, 늘어나면 확대. */ function ProportionalRenderer({ components, @@ -48,7 +47,7 @@ function ProportionalRenderer({ }, []); const topLevel = components.filter((c) => !c.parentId); - const scale = containerW > 0 ? containerW / canvasWidth : 1; + const ratio = containerW > 0 ? containerW / canvasWidth : 1; const maxBottom = topLevel.reduce((max, c) => { const bottom = c.position.y + (c.size?.height || 40); @@ -59,41 +58,30 @@ function ProportionalRenderer({
0 ? `${maxBottom * scale}px` : "200px" }} + className="bg-background relative w-full overflow-x-hidden" + style={{ minHeight: containerW > 0 ? `${maxBottom * ratio}px` : "200px" }} > - {containerW > 0 && ( -
- {topLevel.map((component) => { - const typeId = getComponentTypeId(component); - return ( -
- {renderComponent(component)} -
- ); - })} -
- )} + {containerW > 0 && + topLevel.map((component) => { + const typeId = getComponentTypeId(component); + return ( +
+ {renderComponent(component)} +
+ ); + })}
); } diff --git a/frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx b/frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx index dc3dccc0..26a5d7c4 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: undefined, // 비율 스케일링 시 래퍼 높이를 정확히 따르도록 제거 + minHeight: "32px", // 🔧 최소 높이를 32px로 줄임 // 커스텀 테두리 스타일 (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 fc244a3d..19a167ae 100644 --- a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx @@ -309,8 +309,11 @@ export const SplitPanelLayoutComponent: React.FC const [leftFilters, setLeftFilters] = useState([]); const [leftGrouping, setLeftGrouping] = useState([]); const [leftColumnVisibility, setLeftColumnVisibility] = useState([]); - const [leftColumnOrder, setLeftColumnOrder] = useState([]); // 🔧 컬럼 순서 - const [leftGroupSumConfig, setLeftGroupSumConfig] = useState(null); // 🆕 그룹별 합산 설정 + const [leftColumnOrder, setLeftColumnOrder] = useState([]); + const [leftGroupSumConfig, setLeftGroupSumConfig] = useState(null); + // 좌측 패널 컬럼 헤더 드래그 + const [leftDraggedColumnIndex, setLeftDraggedColumnIndex] = useState(null); + const [leftDropTargetColumnIndex, setLeftDropTargetColumnIndex] = useState(null); const [rightFilters, setRightFilters] = useState([]); const [rightGrouping, setRightGrouping] = useState([]); const [rightColumnVisibility, setRightColumnVisibility] = useState([]); @@ -2520,6 +2523,47 @@ export const SplitPanelLayoutComponent: React.FC } }, [selectedLeftItem, customLeftSelectedData, componentConfig, companyCode, toast, loadLeftData]); + // 좌측 패널 컬럼 헤더 드래그 + const handleLeftColumnDragStart = useCallback( + (e: React.DragEvent, columnIndex: number) => { + setLeftDraggedColumnIndex(columnIndex); + e.dataTransfer.effectAllowed = "move"; + e.dataTransfer.setData("text/plain", `left-col-${columnIndex}`); + }, + [], + ); + const handleLeftColumnDragOver = useCallback((e: React.DragEvent, columnIndex: number) => { + e.preventDefault(); + e.dataTransfer.dropEffect = "move"; + setLeftDropTargetColumnIndex(columnIndex); + }, []); + const handleLeftColumnDragEnd = useCallback(() => { + setLeftDraggedColumnIndex(null); + setLeftDropTargetColumnIndex(null); + }, []); + const handleLeftColumnDrop = useCallback( + (e: React.DragEvent, targetIndex: number) => { + e.preventDefault(); + const fromIdx = leftDraggedColumnIndex; + if (fromIdx === null || fromIdx === targetIndex) { + handleLeftColumnDragEnd(); + return; + } + const leftColumns = componentConfig.leftPanel?.columns || []; + const colNames = leftColumns + .filter((c: any) => typeof c === "string" || c.name || c.columnName) + .map((c: any) => typeof c === "string" ? c : c.name || c.columnName); + if (colNames.length > 0 && fromIdx >= 0 && fromIdx < colNames.length && targetIndex >= 0 && targetIndex < colNames.length) { + const reordered = [...colNames]; + const [removed] = reordered.splice(fromIdx, 1); + reordered.splice(targetIndex, 0, removed); + setLeftColumnOrder(reordered); + } + handleLeftColumnDragEnd(); + }, + [leftDraggedColumnIndex, componentConfig, handleLeftColumnDragEnd], + ); + // 우측 패널 컬럼 헤더 드래그 (디자인 모드에서 컬럼 순서 변경) const handleRightColumnDragStart = useCallback( (e: React.DragEvent, columnIndex: number, source: "main" | number) => { @@ -3568,6 +3612,7 @@ export const SplitPanelLayoutComponent: React.FC (componentConfig.leftPanel?.showEdit !== false) || (componentConfig.leftPanel?.showDelete !== false) ); + const canDragLeftGroupedColumns = !isDesignMode && columnsToShow.length > 1; if (groupedLeftData.length > 0) { return (
@@ -3579,18 +3624,33 @@ export const SplitPanelLayoutComponent: React.FC 100 ? `${leftTotalColWidth}%` : '100%' }}> - {columnsToShow.map((col, idx) => ( + {columnsToShow.map((col, idx) => { + const isDropTarget = canDragLeftGroupedColumns && leftDropTargetColumnIndex === idx; + const isDragging = canDragLeftGroupedColumns && leftDraggedColumnIndex === idx; + return ( - ))} + ); + })} {hasGroupedLeftActions && ( @@ -3671,23 +3731,39 @@ export const SplitPanelLayoutComponent: React.FC (componentConfig.leftPanel?.showEdit !== false) || (componentConfig.leftPanel?.showDelete !== false) ); + const canDragLeftColumns = !isDesignMode && columnsToShow.length > 1; return (
canDragLeftGroupedColumns && handleLeftColumnDragStart(e, idx)} + onDragOver={(e) => canDragLeftGroupedColumns && handleLeftColumnDragOver(e, idx)} + onDragEnd={handleLeftColumnDragEnd} + onDrop={(e) => canDragLeftGroupedColumns && handleLeftColumnDrop(e, idx)} > + {canDragLeftGroupedColumns && } {col.label}
100 ? `${leftTotalColWidth}%` : '100%' }}> - {columnsToShow.map((col, idx) => ( + {columnsToShow.map((col, idx) => { + const isDropTarget = canDragLeftColumns && leftDropTargetColumnIndex === idx; + const isDragging = canDragLeftColumns && leftDraggedColumnIndex === idx; + return ( - ))} + ); + })} {hasLeftTableActions && ( @@ -4161,9 +4237,11 @@ export const SplitPanelLayoutComponent: React.FC // 테이블 모드로 표시 (행 클릭 시 상세 정보 펼치기) if (currentTabConfig?.displayMode === "table") { const hasTabActions = currentTabConfig?.showEdit || currentTabConfig?.showDelete; - // showInSummary가 false가 아닌 것만 메인 테이블에 표시 - const tabSummaryColumns = tabColumns.filter((col: any) => col.showInSummary !== false); const tabIndex = activeTabIndex - 1; + let tabSummaryColumns = tabColumns.filter((col: any) => col.showInSummary !== false); + if (!isDesignMode) { + tabSummaryColumns = applyRuntimeOrder(tabSummaryColumns, tabIndex); + } const canDragTabColumns = tabSummaryColumns.length > 0; return (
@@ -4188,7 +4266,7 @@ export const SplitPanelLayoutComponent: React.FC onDragEnd={handleRightColumnDragEnd} onDrop={(e) => canDragTabColumns && handleRightColumnDrop(e, idx, tabIndex)} > - {canDragTabColumns && } + {canDragTabColumns && } {col.label || col.name} ); @@ -4298,9 +4376,11 @@ export const SplitPanelLayoutComponent: React.FC // 리스트 모드도 테이블형으로 통일 (행 클릭 시 상세 정보 표시) { const hasTabActions = currentTabConfig?.showEdit || currentTabConfig?.showDelete; - // showInSummary가 false가 아닌 것만 메인 테이블에 표시 - const listSummaryColumns = tabColumns.filter((col: any) => col.showInSummary !== false); const listTabIndex = activeTabIndex - 1; + let listSummaryColumns = tabColumns.filter((col: any) => col.showInSummary !== false); + if (!isDesignMode) { + listSummaryColumns = applyRuntimeOrder(listSummaryColumns, listTabIndex); + } const canDragListTabColumns = listSummaryColumns.length > 0; return (
@@ -4325,7 +4405,7 @@ export const SplitPanelLayoutComponent: React.FC onDragEnd={handleRightColumnDragEnd} onDrop={(e) => canDragListTabColumns && handleRightColumnDrop(e, idx, listTabIndex)} > - {canDragListTabColumns && } + {canDragListTabColumns && } {col.label || col.name} ); @@ -4752,7 +4832,7 @@ export const SplitPanelLayoutComponent: React.FC onDragEnd={handleRightColumnDragEnd} onDrop={(e) => isDraggable && handleRightColumnDrop(e, configColIndex, "main")} > - {isDraggable && } + {isDraggable && } {col.label} );
canDragLeftColumns && handleLeftColumnDragStart(e, idx)} + onDragOver={(e) => canDragLeftColumns && handleLeftColumnDragOver(e, idx)} + onDragEnd={handleLeftColumnDragEnd} + onDrop={(e) => canDragLeftColumns && handleLeftColumnDrop(e, idx)} > + {canDragLeftColumns && } {col.label}