상단 헤더 제거
This commit is contained in:
parent
7713d4073c
commit
e33664015a
|
|
@ -308,7 +308,7 @@ function ScreenViewPage() {
|
|||
<TableOptionsProvider>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="bg-background flex h-full w-full items-center justify-center overflow-auto pt-8"
|
||||
className="bg-background flex h-full w-full items-center justify-center overflow-auto"
|
||||
>
|
||||
{/* 레이아웃 준비 중 로딩 표시 */}
|
||||
{!layoutReady && (
|
||||
|
|
|
|||
|
|
@ -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,21 +408,7 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
|||
const uiMenus = convertMenuToUI(currentMenus, user as ExtendedUserInfo);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen flex-col bg-white">
|
||||
{/* MainHeader 컴포넌트 사용 */}
|
||||
<MainHeader
|
||||
user={user}
|
||||
onSidebarToggle={() => {
|
||||
// 모바일에서만 토글 동작
|
||||
if (isMobile) {
|
||||
setSidebarOpen(!sidebarOpen);
|
||||
}
|
||||
}}
|
||||
onProfileClick={openProfileModal}
|
||||
onLogout={handleLogout}
|
||||
/>
|
||||
|
||||
<div className="flex flex-1 pt-14">
|
||||
<div className="flex h-screen bg-white">
|
||||
{/* 모바일 사이드바 오버레이 */}
|
||||
{sidebarOpen && isMobile && (
|
||||
<div className="fixed inset-0 z-30 bg-black/50 lg:hidden" onClick={() => setSidebarOpen(false)} />
|
||||
|
|
@ -421,11 +418,20 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
|||
<aside
|
||||
className={`${
|
||||
isMobile
|
||||
? (sidebarOpen ? "translate-x-0" : "-translate-x-full") + " fixed top-14 left-0 z-40"
|
||||
: "relative top-0 z-auto translate-x-0"
|
||||
} flex h-[calc(100vh-3.5rem)] w-[200px] flex-col border-r border-slate-200 bg-white transition-transform duration-300`}
|
||||
? (sidebarOpen ? "translate-x-0" : "-translate-x-full") + " fixed top-0 left-0 z-40"
|
||||
: "relative z-auto translate-x-0"
|
||||
} flex h-screen w-[200px] flex-col border-r border-slate-200 bg-white transition-transform duration-300`}
|
||||
>
|
||||
{/* 사이드바 상단 - Admin/User 모드 전환 버튼 (관리자만) */}
|
||||
{/* 사이드바 최상단 - 로고 + 모바일 햄버거 메뉴 */}
|
||||
<div className="flex h-14 items-center justify-between border-b border-slate-200 px-4">
|
||||
<Logo />
|
||||
{/* 모바일 햄버거 메뉴 버튼 */}
|
||||
<div className="lg:hidden">
|
||||
<SideMenu onSidebarToggle={() => setSidebarOpen(!sidebarOpen)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Admin/User 모드 전환 버튼 (관리자만) */}
|
||||
{((user as ExtendedUserInfo)?.userType === "SUPER_ADMIN" ||
|
||||
(user as ExtendedUserInfo)?.userType === "COMPANY_ADMIN" ||
|
||||
(user as ExtendedUserInfo)?.userType === "admin") && (
|
||||
|
|
@ -453,6 +459,7 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* 메뉴 영역 */}
|
||||
<div className="flex-1 overflow-y-auto py-4">
|
||||
<nav className="space-y-1 px-3">
|
||||
{loading ? (
|
||||
|
|
@ -466,13 +473,87 @@ function AppLayoutInner({ children }: AppLayoutProps) {
|
|||
)}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* 사이드바 하단 - 사용자 프로필 */}
|
||||
<div className="border-t border-slate-200 p-3">
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button className="flex w-full items-center gap-3 rounded-lg px-2 py-2 text-left transition-colors hover:bg-slate-100">
|
||||
{/* 프로필 아바타 */}
|
||||
<div className="relative flex h-9 w-9 shrink-0 overflow-hidden rounded-full">
|
||||
{user.photo && user.photo.trim() !== "" && user.photo !== "null" ? (
|
||||
<img
|
||||
src={user.photo}
|
||||
alt={user.userName || "User"}
|
||||
className="aspect-square h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center rounded-full bg-slate-200 text-sm font-semibold text-slate-700">
|
||||
{user.userName?.substring(0, 1)?.toUpperCase() || "U"}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* 사용자 정보 */}
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-sm font-medium text-slate-900">
|
||||
{user.userName || "사용자"}
|
||||
</p>
|
||||
<p className="truncate text-xs text-slate-500">
|
||||
{user.deptName || user.email || user.userId}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56" align="start" side="top">
|
||||
<DropdownMenuLabel className="font-normal">
|
||||
<div className="flex items-center space-x-3">
|
||||
{/* 프로필 사진 표시 */}
|
||||
<div className="relative flex h-12 w-12 shrink-0 overflow-hidden rounded-full">
|
||||
{user.photo && user.photo.trim() !== "" && user.photo !== "null" ? (
|
||||
<img
|
||||
src={user.photo}
|
||||
alt={user.userName || "User"}
|
||||
className="aspect-square h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center rounded-full bg-slate-200 text-base font-semibold text-slate-700">
|
||||
{user.userName?.substring(0, 1)?.toUpperCase() || "U"}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 사용자 정보 */}
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-sm leading-none font-medium">
|
||||
{user.userName || "사용자"} ({user.userId || ""})
|
||||
</p>
|
||||
<p className="text-muted-foreground text-xs leading-none font-semibold">{user.email || ""}</p>
|
||||
<p className="text-muted-foreground text-xs leading-none font-semibold">
|
||||
{user.deptName && user.positionName
|
||||
? `${user.deptName}, ${user.positionName}`
|
||||
: user.deptName || user.positionName || "부서 정보 없음"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={openProfileModal}>
|
||||
<User className="mr-2 h-4 w-4" />
|
||||
<span>프로필</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={handleLogout}>
|
||||
<LogOut className="mr-2 h-4 w-4" />
|
||||
<span>로그아웃</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* 가운데 컨텐츠 영역 - 스크롤 가능 */}
|
||||
<main className="h-[calc(100vh-3.5rem)] min-w-0 flex-1 overflow-auto bg-white">
|
||||
<main className="h-screen min-w-0 flex-1 overflow-auto bg-white">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{/* 프로필 수정 모달 */}
|
||||
<ProfileModal
|
||||
|
|
|
|||
|
|
@ -1265,7 +1265,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
[layout, screenResolution, saveToHistory],
|
||||
);
|
||||
|
||||
// 해상도 변경 핸들러 (자동 스케일링 포함)
|
||||
// 해상도 변경 핸들러 (컴포넌트 크기/위치 유지)
|
||||
const handleResolutionChange = useCallback(
|
||||
(newResolution: ScreenResolution) => {
|
||||
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],
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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<TabItem[]>(tabs);
|
||||
const [loadingScreens, setLoadingScreens] = useState<Record<string, boolean>>({});
|
||||
const [screenLayouts, setScreenLayouts] = useState<Record<number, any>>({});
|
||||
// 🆕 한 번이라도 선택된 탭 추적 (지연 로딩 + 캐싱)
|
||||
const [mountedTabs, setMountedTabs] = useState<Set<string>>(() => new Set([getInitialTab()]));
|
||||
|
||||
// 컴포넌트 탭 목록 변경 시 동기화
|
||||
useEffect(() => {
|
||||
|
|
@ -110,6 +113,14 @@ export function TabsWidget({ component, className, style, menuObjid }: TabsWidge
|
|||
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);
|
||||
console.log("🔍 선택된 탭 정보:", { tab, hasScreenId: !!tab?.screenId, screenId: tab?.screenId });
|
||||
|
|
@ -191,9 +202,26 @@ export function TabsWidget({ component, className, style, menuObjid }: TabsWidge
|
|||
</TabsList>
|
||||
</div>
|
||||
|
||||
{/* 🆕 forceMount + CSS 숨김으로 탭 전환 시 리렌더링 방지 */}
|
||||
<div className="relative flex-1 overflow-hidden">
|
||||
{visibleTabs.map((tab) => (
|
||||
<TabsContent key={tab.id} value={tab.id} className="h-full">
|
||||
{visibleTabs.map((tab) => {
|
||||
// 한 번도 선택되지 않은 탭은 렌더링하지 않음 (지연 로딩)
|
||||
const shouldRender = mountedTabs.has(tab.id);
|
||||
const isActive = selectedTab === tab.id;
|
||||
|
||||
return (
|
||||
<TabsContent
|
||||
key={tab.id}
|
||||
value={tab.id}
|
||||
forceMount // 🆕 DOM에 항상 유지
|
||||
className={cn(
|
||||
"h-full",
|
||||
!isActive && "hidden" // 🆕 비활성 탭은 CSS로 숨김
|
||||
)}
|
||||
>
|
||||
{/* 한 번 마운트된 탭만 내용 렌더링 */}
|
||||
{shouldRender && (
|
||||
<>
|
||||
{tab.screenId ? (
|
||||
loadingScreens[tab.screenId] ? (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
|
|
@ -205,11 +233,14 @@ export function TabsWidget({ component, className, style, menuObjid }: TabsWidge
|
|||
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;
|
||||
|
|
@ -238,7 +269,7 @@ export function TabsWidget({ component, className, style, menuObjid }: TabsWidge
|
|||
id: tab.screenId,
|
||||
tableName: layoutData.tableName,
|
||||
}}
|
||||
menuObjid={menuObjid} // 🆕 부모의 menuObjid 전달
|
||||
menuObjid={menuObjid}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -255,8 +286,11 @@ export function TabsWidget({ component, className, style, menuObjid }: TabsWidge
|
|||
<p className="text-muted-foreground text-sm">연결된 화면이 없습니다</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</TabsContent>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue