diff --git a/frontend/app/(main)/screens/[screenId]/page.tsx b/frontend/app/(main)/screens/[screenId]/page.tsx index 8ab31ff7..5ce253cb 100644 --- a/frontend/app/(main)/screens/[screenId]/page.tsx +++ b/frontend/app/(main)/screens/[screenId]/page.tsx @@ -308,7 +308,7 @@ function ScreenViewPage() {
{/* 레이아웃 준비 중 로딩 표시 */} {!layoutReady && ( diff --git a/frontend/components/layout/AppLayout.tsx b/frontend/components/layout/AppLayout.tsx index 8394cd6d..faba6df5 100644 --- a/frontend/components/layout/AppLayout.tsx +++ b/frontend/components/layout/AppLayout.tsx @@ -15,6 +15,8 @@ import { ChevronDown, ChevronRight, UserCheck, + LogOut, + User, } from "lucide-react"; import { useMenu } from "@/contexts/MenuContext"; import { useAuth } from "@/hooks/useAuth"; @@ -22,8 +24,17 @@ import { useProfile } from "@/hooks/useProfile"; import { MenuItem } from "@/lib/api/menu"; import { menuScreenApi } from "@/lib/api/screen"; import { toast } from "sonner"; -import { MainHeader } from "./MainHeader"; import { ProfileModal } from "./ProfileModal"; +import { Logo } from "./Logo"; +import { SideMenu } from "./SideMenu"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; // useAuth의 UserInfo 타입을 확장 interface ExtendedUserInfo { @@ -397,82 +408,152 @@ function AppLayoutInner({ children }: AppLayoutProps) { const uiMenus = convertMenuToUI(currentMenus, user as ExtendedUserInfo); return ( -
- {/* MainHeader 컴포넌트 사용 */} - { - // 모바일에서만 토글 동작 - if (isMobile) { - setSidebarOpen(!sidebarOpen); - } - }} - onProfileClick={openProfileModal} - onLogout={handleLogout} - /> +
+ {/* 모바일 사이드바 오버레이 */} + {sidebarOpen && isMobile && ( +
setSidebarOpen(false)} /> + )} -
- {/* 모바일 사이드바 오버레이 */} - {sidebarOpen && isMobile && ( -
setSidebarOpen(false)} /> + {/* 왼쪽 사이드바 */} +
+ + + + + 프로필 + + + + 로그아웃 + + + +
+ + + {/* 가운데 컨텐츠 영역 - 스크롤 가능 */} +
+ {children} +
{/* 프로필 수정 모달 */} { const oldWidth = screenResolution.width; @@ -1273,122 +1273,28 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD const newWidth = newResolution.width; const newHeight = newResolution.height; - console.log("📱 해상도 변경 시작:", { + console.log("📱 해상도 변경:", { from: `${oldWidth}x${oldHeight}`, to: `${newWidth}x${newHeight}`, - hasComponents: layout.components.length > 0, - snapToGrid: layout.gridSettings?.snapToGrid || false, + componentsCount: layout.components.length, }); setScreenResolution(newResolution); - // 컴포넌트가 없으면 해상도만 변경 - if (layout.components.length === 0) { - const updatedLayout = { - ...layout, - screenResolution: newResolution, - }; - setLayout(updatedLayout); - saveToHistory(updatedLayout); - console.log("✅ 해상도 변경 완료 (컴포넌트 없음)"); - return; - } - - // 비율 계산 - const scaleX = newWidth / oldWidth; - const scaleY = newHeight / oldHeight; - - console.log("📐 스케일링 비율:", { - scaleX: `${(scaleX * 100).toFixed(2)}%`, - scaleY: `${(scaleY * 100).toFixed(2)}%`, - }); - - // 컴포넌트 재귀적으로 스케일링하는 함수 - const scaleComponent = (comp: ComponentData): ComponentData => { - // 위치 스케일링 - const scaledPosition = { - x: comp.position.x * scaleX, - y: comp.position.y * scaleY, - z: comp.position.z || 1, - }; - - // 크기 스케일링 - const scaledSize = { - width: comp.size.width * scaleX, - height: comp.size.height * scaleY, - }; - - return { - ...comp, - position: scaledPosition, - size: scaledSize, - }; - }; - - // 모든 컴포넌트 스케일링 (그룹의 자식도 자동으로 스케일링됨) - const scaledComponents = layout.components.map(scaleComponent); - - console.log("🔄 컴포넌트 스케일링 완료:", { - totalComponents: scaledComponents.length, - groupComponents: scaledComponents.filter((c) => c.type === "group").length, - note: "그룹의 자식 컴포넌트도 모두 스케일링됨", - }); - - // 격자 스냅이 활성화된 경우 격자에 맞춰 재조정 - let finalComponents = scaledComponents; - if (layout.gridSettings?.snapToGrid) { - const newGridInfo = calculateGridInfo(newWidth, newHeight, { - columns: layout.gridSettings.columns, - gap: layout.gridSettings.gap, - padding: layout.gridSettings.padding, - snapToGrid: layout.gridSettings.snapToGrid || false, - }); - - const gridUtilSettings = { - columns: layout.gridSettings.columns, - gap: layout.gridSettings.gap, - padding: layout.gridSettings.padding, - snapToGrid: true, - }; - - finalComponents = scaledComponents.map((comp) => { - const snappedPosition = snapToGrid(comp.position, newGridInfo, gridUtilSettings); - const snappedSize = snapSizeToGrid(comp.size, newGridInfo, gridUtilSettings); - - // gridColumns 재계산 - const adjustedGridColumns = adjustGridColumnsFromSize({ size: snappedSize }, newGridInfo, gridUtilSettings); - - return { - ...comp, - position: snappedPosition, - size: snappedSize, - gridColumns: adjustedGridColumns, - }; - }); - - console.log("🧲 격자 스냅 적용 완료"); - } - + // 해상도만 변경하고 컴포넌트 크기/위치는 그대로 유지 const updatedLayout = { ...layout, - components: finalComponents, screenResolution: newResolution, }; setLayout(updatedLayout); saveToHistory(updatedLayout); - toast.success(`해상도 변경 완료! ${scaledComponents.length}개 컴포넌트가 자동으로 조정되었습니다.`, { + toast.success(`해상도가 변경되었습니다.`, { description: `${oldWidth}×${oldHeight} → ${newWidth}×${newHeight}`, }); - console.log("✅ 해상도 변경 완료:", { - newResolution: `${newWidth}x${newHeight}`, - scaledComponents: finalComponents.length, - scaleX: `${(scaleX * 100).toFixed(2)}%`, - scaleY: `${(scaleY * 100).toFixed(2)}%`, - note: "모든 컴포넌트가 비율에 맞게 자동 조정됨", - }); + console.log("✅ 해상도 변경 완료 (컴포넌트 크기/위치 유지)"); }, [layout, saveToHistory, screenResolution], ); diff --git a/frontend/components/screen/widgets/TabsWidget.tsx b/frontend/components/screen/widgets/TabsWidget.tsx index 73b53783..200e2db3 100644 --- a/frontend/components/screen/widgets/TabsWidget.tsx +++ b/frontend/components/screen/widgets/TabsWidget.tsx @@ -1,11 +1,12 @@ "use client"; -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useMemo } from "react"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Button } from "@/components/ui/button"; import { X, Loader2 } from "lucide-react"; import type { TabsComponent, TabItem } from "@/types/screen-management"; import { InteractiveScreenViewerDynamic } from "@/components/screen/InteractiveScreenViewerDynamic"; +import { cn } from "@/lib/utils"; interface TabsWidgetProps { component: TabsComponent; @@ -48,6 +49,8 @@ export function TabsWidget({ component, className, style, menuObjid }: TabsWidge const [visibleTabs, setVisibleTabs] = useState(tabs); const [loadingScreens, setLoadingScreens] = useState>({}); const [screenLayouts, setScreenLayouts] = useState>({}); + // 🆕 한 번이라도 선택된 탭 추적 (지연 로딩 + 캐싱) + const [mountedTabs, setMountedTabs] = useState>(() => new Set([getInitialTab()])); // 컴포넌트 탭 목록 변경 시 동기화 useEffect(() => { @@ -109,6 +112,14 @@ export function TabsWidget({ component, className, style, menuObjid }: TabsWidge const handleTabChange = (tabId: string) => { console.log("🔄 탭 변경:", tabId); setSelectedTab(tabId); + + // 🆕 마운트된 탭 목록에 추가 (한 번 마운트되면 유지) + setMountedTabs(prev => { + if (prev.has(tabId)) return prev; + const newSet = new Set(prev); + newSet.add(tabId); + return newSet; + }); // 해당 탭의 화면 로드 const tab = visibleTabs.find((t) => t.id === tabId); @@ -191,72 +202,95 @@ export function TabsWidget({ component, className, style, menuObjid }: TabsWidge
+ {/* 🆕 forceMount + CSS 숨김으로 탭 전환 시 리렌더링 방지 */}
- {visibleTabs.map((tab) => ( - - {tab.screenId ? ( - loadingScreens[tab.screenId] ? ( -
- - 화면 로딩 중... -
- ) : screenLayouts[tab.screenId] ? ( - (() => { - const layoutData = screenLayouts[tab.screenId]; - const { components = [], screenResolution } = layoutData; - - console.log("🎯 렌더링할 화면 데이터:", { - screenId: tab.screenId, - componentsCount: components.length, - screenResolution, - }); - - const designWidth = screenResolution?.width || 1920; - const designHeight = screenResolution?.height || 1080; - - return ( -
-
- {components.map((component: any) => ( - - ))} + {visibleTabs.map((tab) => { + // 한 번도 선택되지 않은 탭은 렌더링하지 않음 (지연 로딩) + const shouldRender = mountedTabs.has(tab.id); + const isActive = selectedTab === tab.id; + + return ( + + {/* 한 번 마운트된 탭만 내용 렌더링 */} + {shouldRender && ( + <> + {tab.screenId ? ( + loadingScreens[tab.screenId] ? ( +
+ + 화면 로딩 중...
+ ) : screenLayouts[tab.screenId] ? ( + (() => { + const layoutData = screenLayouts[tab.screenId]; + const { components = [], screenResolution } = layoutData; + + // 비활성 탭은 로그 생략 + if (isActive) { + console.log("🎯 렌더링할 화면 데이터:", { + screenId: tab.screenId, + componentsCount: components.length, + screenResolution, + }); + } + + const designWidth = screenResolution?.width || 1920; + const designHeight = screenResolution?.height || 1080; + + return ( +
+
+ {components.map((component: any) => ( + + ))} +
+
+ ); + })() + ) : ( +
+

화면을 불러올 수 없습니다

+
+ ) + ) : ( +
+

연결된 화면이 없습니다

- ); - })() - ) : ( -
-

화면을 불러올 수 없습니다

-
- ) - ) : ( -
-

연결된 화면이 없습니다

-
- )} -
- ))} + )} + + )} + + ); + })}