상단 헤더 제거

This commit is contained in:
kjs 2025-12-03 10:03:24 +09:00
parent 7713d4073c
commit e33664015a
4 changed files with 257 additions and 236 deletions

View File

@ -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 && (

View File

@ -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 (
<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 h-screen bg-white">
{/* 모바일 사이드바 오버레이 */}
{sidebarOpen && isMobile && (
<div className="fixed inset-0 z-30 bg-black/50 lg:hidden" onClick={() => setSidebarOpen(false)} />
)}
<div className="flex flex-1 pt-14">
{/* 모바일 사이드바 오버레이 */}
{sidebarOpen && isMobile && (
<div className="fixed inset-0 z-30 bg-black/50 lg:hidden" onClick={() => setSidebarOpen(false)} />
{/* 왼쪽 사이드바 */}
<aside
className={`${
isMobile
? (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`}
>
{/* 사이드바 최상단 - 로고 + 모바일 햄버거 메뉴 */}
<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") && (
<div className="border-b border-slate-200 p-3">
<Button
onClick={handleModeSwitch}
className={`flex w-full items-center justify-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-colors duration-200 hover:cursor-pointer ${
isAdminMode
? "border border-orange-200 bg-orange-50 text-orange-700 hover:bg-orange-100"
: "border-primary/20 bg-accent hover:bg-primary/20 border text-blue-700"
}`}
>
{isAdminMode ? (
<>
<UserCheck className="h-4 w-4" />
</>
) : (
<>
<Shield className="h-4 w-4" />
</>
)}
</Button>
</div>
)}
{/* 왼쪽 사이드바 */}
<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`}
>
{/* 사이드바 상단 - Admin/User 모드 전환 버튼 (관리자만) */}
{((user as ExtendedUserInfo)?.userType === "SUPER_ADMIN" ||
(user as ExtendedUserInfo)?.userType === "COMPANY_ADMIN" ||
(user as ExtendedUserInfo)?.userType === "admin") && (
<div className="border-b border-slate-200 p-3">
<Button
onClick={handleModeSwitch}
className={`flex w-full items-center justify-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-colors duration-200 hover:cursor-pointer ${
isAdminMode
? "border border-orange-200 bg-orange-50 text-orange-700 hover:bg-orange-100"
: "border-primary/20 bg-accent hover:bg-primary/20 border text-blue-700"
}`}
>
{isAdminMode ? (
<>
<UserCheck className="h-4 w-4" />
</>
) : (
<>
<Shield className="h-4 w-4" />
</>
)}
</Button>
</div>
)}
{/* 메뉴 영역 */}
<div className="flex-1 overflow-y-auto py-4">
<nav className="space-y-1 px-3">
{loading ? (
<div className="animate-pulse space-y-2">
{[...Array(5)].map((_, i) => (
<div key={i} className="h-8 rounded bg-slate-200"></div>
))}
</div>
) : (
uiMenus.map((menu) => renderMenu(menu))
)}
</nav>
</div>
<div className="flex-1 overflow-y-auto py-4">
<nav className="space-y-1 px-3">
{loading ? (
<div className="animate-pulse space-y-2">
{[...Array(5)].map((_, i) => (
<div key={i} className="h-8 rounded bg-slate-200"></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>
) : (
uiMenus.map((menu) => renderMenu(menu))
)}
</nav>
</div>
</aside>
{/* 사용자 정보 */}
<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>
{/* 가운데 컨텐츠 영역 - 스크롤 가능 */}
<main className="h-[calc(100vh-3.5rem)] min-w-0 flex-1 overflow-auto bg-white">
{children}
</main>
</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-screen min-w-0 flex-1 overflow-auto bg-white">
{children}
</main>
{/* 프로필 수정 모달 */}
<ProfileModal

View File

@ -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],
);

View File

@ -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,72 +202,95 @@ 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">
{tab.screenId ? (
loadingScreens[tab.screenId] ? (
<div className="flex h-full w-full items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
<span className="text-muted-foreground ml-2"> ...</span>
</div>
) : screenLayouts[tab.screenId] ? (
(() => {
const layoutData = screenLayouts[tab.screenId];
const { components = [], screenResolution } = layoutData;
{visibleTabs.map((tab) => {
// 한 번도 선택되지 않은 탭은 렌더링하지 않음 (지연 로딩)
const shouldRender = mountedTabs.has(tab.id);
const isActive = selectedTab === tab.id;
console.log("🎯 렌더링할 화면 데이터:", {
screenId: tab.screenId,
componentsCount: components.length,
screenResolution,
});
const designWidth = screenResolution?.width || 1920;
const designHeight = screenResolution?.height || 1080;
return (
<div
className="relative h-full w-full overflow-auto bg-background"
style={{
minHeight: `${designHeight}px`,
}}
>
<div
className="relative"
style={{
width: `${designWidth}px`,
height: `${designHeight}px`,
margin: "0 auto",
}}
>
{components.map((component: any) => (
<InteractiveScreenViewerDynamic
key={component.id}
component={component}
allComponents={components}
screenInfo={{
id: tab.screenId,
tableName: layoutData.tableName,
}}
menuObjid={menuObjid} // 🆕 부모의 menuObjid 전달
/>
))}
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">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
<span className="text-muted-foreground ml-2"> ...</span>
</div>
) : 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 (
<div
className="relative h-full w-full overflow-auto bg-background"
style={{
minHeight: `${designHeight}px`,
}}
>
<div
className="relative"
style={{
width: `${designWidth}px`,
height: `${designHeight}px`,
margin: "0 auto",
}}
>
{components.map((component: any) => (
<InteractiveScreenViewerDynamic
key={component.id}
component={component}
allComponents={components}
screenInfo={{
id: tab.screenId,
tableName: layoutData.tableName,
}}
menuObjid={menuObjid}
/>
))}
</div>
</div>
);
})()
) : (
<div className="flex h-full w-full items-center justify-center">
<p className="text-muted-foreground text-sm"> </p>
</div>
)
) : (
<div className="flex h-full w-full items-center justify-center rounded border-2 border-dashed border-gray-300 bg-gray-50">
<p className="text-muted-foreground text-sm"> </p>
</div>
);
})()
) : (
<div className="flex h-full w-full items-center justify-center">
<p className="text-muted-foreground text-sm"> </p>
</div>
)
) : (
<div className="flex h-full w-full items-center justify-center rounded border-2 border-dashed border-gray-300 bg-gray-50">
<p className="text-muted-foreground text-sm"> </p>
</div>
)}
</TabsContent>
))}
)}
</>
)}
</TabsContent>
);
})}
</div>
</Tabs>
</div>