From 8bc8df4eb86e1252b936773d455fb65138e77cb0 Mon Sep 17 00:00:00 2001 From: kjs Date: Tue, 14 Oct 2025 13:27:02 +0900 Subject: [PATCH] =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=EB=84=88=EB=B9=84=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/(main)/screens/[screenId]/page.tsx | 12 +- .../screen/InteractiveScreenViewer.tsx | 8 +- .../screen/RealtimePreviewDynamic.tsx | 44 +++- .../screen/panels/PropertiesPanel.tsx | 194 ++++++++++++++---- 4 files changed, 195 insertions(+), 63 deletions(-) diff --git a/frontend/app/(main)/screens/[screenId]/page.tsx b/frontend/app/(main)/screens/[screenId]/page.tsx index 7d92dc1a..f0ddf80c 100644 --- a/frontend/app/(main)/screens/[screenId]/page.tsx +++ b/frontend/app/(main)/screens/[screenId]/page.tsx @@ -178,8 +178,8 @@ export default function ScreenViewPage() { position: "absolute", left: `${component.position.x}px`, top: `${component.position.y}px`, - width: `${component.size.width}px`, - height: `${component.size.height}px`, + width: component.style?.width || `${component.size.width}px`, + height: component.style?.height || `${component.size.height}px`, zIndex: component.position.z || 1, backgroundColor: (component as any).backgroundColor || "rgba(59, 130, 246, 0.05)", border: (component as any).border || "1px solid rgba(59, 130, 246, 0.2)", @@ -203,8 +203,8 @@ export default function ScreenViewPage() { position: "absolute", left: `${child.position.x}px`, top: `${child.position.y}px`, - width: `${child.size.width}px`, - height: `${child.size.height}px`, + width: child.style?.width || `${child.size.width}px`, + height: child.style?.height || `${child.size.height}px`, zIndex: child.position.z || 1, }} > @@ -277,8 +277,8 @@ export default function ScreenViewPage() { position: "absolute", left: `${component.position.x}px`, top: `${component.position.y}px`, - width: `${component.size.width}px`, - height: `${component.size.height}px`, + width: component.style?.width || `${component.size.width}px`, + height: component.style?.height || `${component.size.height}px`, zIndex: component.position.z || 1, }} onMouseEnter={() => { diff --git a/frontend/components/screen/InteractiveScreenViewer.tsx b/frontend/components/screen/InteractiveScreenViewer.tsx index d98b9684..fca43a6c 100644 --- a/frontend/components/screen/InteractiveScreenViewer.tsx +++ b/frontend/components/screen/InteractiveScreenViewer.tsx @@ -1698,8 +1698,8 @@ export const InteractiveScreenViewer: React.FC = ( position: "absolute", left: `${child.position.x - component.position.x}px`, top: `${child.position.y - component.position.y}px`, - width: `${child.size.width}px`, - height: `${child.size.height}px`, + width: child.style?.width || `${child.size.width}px`, + height: child.style?.height || `${child.size.height}px`, zIndex: child.position.z || 1, }} > @@ -1828,8 +1828,8 @@ export const InteractiveScreenViewer: React.FC = ( style={{ left: `${popupComponent.position.x}px`, top: `${popupComponent.position.y}px`, - width: `${popupComponent.size.width}px`, - height: `${popupComponent.size.height}px`, + width: popupComponent.style?.width || `${popupComponent.size.width}px`, + height: popupComponent.style?.height || `${popupComponent.size.height}px`, zIndex: Math.min(popupComponent.position.z || 1, 20), // 최대 z-index 20으로 제한 }} > diff --git a/frontend/components/screen/RealtimePreviewDynamic.tsx b/frontend/components/screen/RealtimePreviewDynamic.tsx index afb720cc..666570e0 100644 --- a/frontend/components/screen/RealtimePreviewDynamic.tsx +++ b/frontend/components/screen/RealtimePreviewDynamic.tsx @@ -90,17 +90,43 @@ export const RealtimePreviewDynamic: React.FC = ({ : {}; // 컴포넌트 기본 스타일 - 레이아웃은 항상 맨 아래 + // 너비 우선순위: style.width > size.width (픽셀값) + const getWidth = () => { + // 1순위: style.width가 있으면 우선 사용 + if (componentStyle?.width) { + return componentStyle.width; + } + + // 2순위: size.width (픽셀) + if (component.componentConfig?.type === "table-list") { + return `${Math.max(size?.width || 120, 120)}px`; + } + + return `${size?.width || 100}px`; + }; + + const getHeight = () => { + // 1순위: style.height가 있으면 우선 사용 + if (componentStyle?.height) { + return componentStyle.height; + } + + // 2순위: size.height (픽셀) + if (component.componentConfig?.type === "table-list") { + return `${Math.max(size?.height || 200, 200)}px`; + } + + return `${size?.height || 40}px`; + }; + const baseStyle = { left: `${position.x}px`, top: `${position.y}px`, - width: component.componentConfig?.type === "table-list" - ? `${Math.max(size?.width || 120, 120)}px` // table-list 디폴트를 그리드 1컬럼 크기로 축소 (120px) - : `${size?.width || 100}px`, - height: component.componentConfig?.type === "table-list" - ? `${Math.max(size?.height || 200, 200)}px` // table-list 디폴트 높이도 축소 (200px) - : `${size?.height || 36}px`, + width: getWidth(), + height: getHeight(), zIndex: component.type === "layout" ? 1 : position.z || 2, // 레이아웃은 z-index 1, 다른 컴포넌트는 2 이상 ...componentStyle, + // style.width와 style.height는 이미 getWidth/getHeight에서 처리했으므로 중복 적용됨 }; const handleClick = (e: React.MouseEvent) => { @@ -134,9 +160,7 @@ export const RealtimePreviewDynamic: React.FC = ({ onDragEnd={handleDragEnd} > {/* 동적 컴포넌트 렌더링 */} -
+
= ({ {/* 선택된 컴포넌트 정보 표시 */} {isSelected && ( -
+
{type === "widget" && (
{getWidgetIcon((component as WidgetComponent).widgetType)} diff --git a/frontend/components/screen/panels/PropertiesPanel.tsx b/frontend/components/screen/panels/PropertiesPanel.tsx index 1589b1c7..5120f123 100644 --- a/frontend/components/screen/panels/PropertiesPanel.tsx +++ b/frontend/components/screen/panels/PropertiesPanel.tsx @@ -223,6 +223,59 @@ const PropertiesPanelComponent: React.FC = ({ (selectedComponent?.type === "widget" ? (selectedComponent as WidgetComponent).widgetType : "text") || "text", }); + // 너비 드롭다운 로컬 상태 - 실시간 업데이트를 위한 별도 관리 + const calculateWidthSpan = (width: string | number | undefined): string => { + if (!width) return "half"; + + if (typeof width === "string" && width.includes("%")) { + const percent = parseFloat(width); + + // 정확한 매핑을 위해 가장 가까운 값 찾기 + // 중복 제거: small(작게) 사용, third(1/3) 사용, twoThirds(2/3) 사용, quarter(1/4) 사용, threeQuarters(3/4) 사용 + const percentToSpan: Record = { + 100: "full", // 12/12 + 91.666667: "eleven-twelfths", // 11/12 + 83.333333: "five-sixths", // 10/12 + 75: "threeQuarters", // 9/12 + 66.666667: "twoThirds", // 8/12 + 58.333333: "seven-twelfths", // 7/12 + 50: "half", // 6/12 + 41.666667: "five-twelfths", // 5/12 + 33.333333: "third", // 4/12 + 25: "quarter", // 3/12 + 16.666667: "small", // 2/12 + 8.333333: "twelfth", // 1/12 + }; + + // 가장 가까운 퍼센트 값 찾기 (오차 범위 ±2% 허용) + let closestSpan = "half"; + let minDiff = Infinity; + + for (const [key, span] of Object.entries(percentToSpan)) { + const diff = Math.abs(percent - parseFloat(key)); + if (diff < minDiff && diff < 5) { + // 5% 오차 범위 내 + minDiff = diff; + closestSpan = span; + } + } + + return closestSpan; + } + + return "half"; + }; + + const [localWidthSpan, setLocalWidthSpan] = useState(() => + calculateWidthSpan(selectedComponent?.style?.width), + ); + + // 컴포넌트 또는 style.width가 변경될 때 로컬 상태 업데이트 + useEffect(() => { + const newSpan = calculateWidthSpan(selectedComponent?.style?.width); + setLocalWidthSpan(newSpan); + }, [selectedComponent?.id, selectedComponent?.style?.width]); + useEffect(() => { selectedComponentRef.current = selectedComponent; onUpdatePropertyRef.current = onUpdateProperty; @@ -676,13 +729,45 @@ const PropertiesPanelComponent: React.FC = ({ {/* 카드 레이아웃은 자동 크기 계산으로 너비/높이 설정 숨김 */} {selectedComponent?.type !== "layout" || (selectedComponent as any)?.layoutType !== "card" ? ( <> - {/* 🆕 컬럼 스팬 선택 (width 대체) */} + {/* 🆕 컬럼 스팬 선택 (width를 퍼센트로 변환) - 기존 UI 유지 */}
- {/* 시각적 프리뷰 */} + {/* 시각적 프리뷰 - 기존 UI 유지, localWidthSpan 기반 */}
{Array.from({ length: 12 }).map((_, i) => { - const spanValue = COLUMN_SPAN_VALUES[selectedComponent.gridColumnSpan || "half"]; - const startCol = selectedComponent.gridColumnStart || 1; - const isActive = i + 1 >= startCol && i + 1 < startCol + spanValue; + // localWidthSpan으로부터 활성 컬럼 계산 + const spanValues: Record = { + // 표준 옵션 + twelfth: 1, + small: 2, + quarter: 3, + third: 4, + "five-twelfths": 5, + half: 6, + "seven-twelfths": 7, + twoThirds: 8, + threeQuarters: 9, + "five-sixths": 10, + "eleven-twelfths": 11, + full: 12, + + // 레거시 호환성 + sixth: 2, + label: 3, + medium: 4, + large: 8, + input: 9, + "two-thirds": 8, + "three-quarters": 9, + }; + + const spanValue = spanValues[localWidthSpan] || 6; + const isActive = i < spanValue; return (
= ({ })}

- {COLUMN_SPAN_VALUES[selectedComponent.gridColumnSpan || "half"]} / 12 컬럼 + {(() => { + const spanValues: Record = { + // 표준 옵션 + twelfth: 1, + small: 2, + quarter: 3, + third: 4, + "five-twelfths": 5, + half: 6, + "seven-twelfths": 7, + twoThirds: 8, + threeQuarters: 9, + "five-sixths": 10, + "eleven-twelfths": 11, + full: 12, + + // 레거시 호환성 + sixth: 2, + label: 3, + medium: 4, + large: 8, + input: 9, + "two-thirds": 8, + "three-quarters": 9, + }; + const cols = spanValues[localWidthSpan] || 6; + return `${cols} / 12 컬럼`; + })()}

- - {/* 고급 설정 */} - - - - - -
- - -

"자동"을 선택하면 이전 컴포넌트 다음에 배치됩니다

-
-
-