feat: 대시보드 레이아웃에 사이드바 메뉴 및 역할 전환 기능 추가
- DashboardLayout에 좌측 사이드바 메뉴 추가 (아이콘 포함) - 상단 헤더에 역할별 대시보드 전환 드롭다운 추가 - menuStore의 persist 설정에서 menuItems 제외 (icon 함수 직렬화 문제 해결) - vite.config.ts HMR 설정을 환경별로 조건부 처리 (로컬/도커 환경 분리) - Lucide React 아이콘 렌더링 패턴 개선 (변수 할당 후 렌더링) 변경 사항: - vite.config.ts: VITE_DOCKER_ENV 환경변수 기반 HMR 설정 - DashboardLayout.tsx: 사이드바, 메뉴, 역할 드롭다운 UI 구현 - menuStore.ts: partialize 옵션으로 menuItems localStorage 저장 제외
This commit is contained in:
@@ -1,57 +1,158 @@
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { SidebarProvider, Sidebar, SidebarHeader, SidebarContent, SidebarMenu, SidebarMenuItem, SidebarMenuButton, SidebarTrigger } from '@/components/ui/sidebar';
|
||||
import { useMenuStore } from '@/store/menuStore';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useEffect } from 'react';
|
||||
import {
|
||||
LayoutDashboard,
|
||||
ShoppingCart,
|
||||
Factory,
|
||||
ClipboardCheck,
|
||||
Package,
|
||||
Database,
|
||||
ChevronDown,
|
||||
Menu,
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function DashboardLayout() {
|
||||
const { menuItems, activeMenu, setActiveMenu, sidebarCollapsed } = useMenuStore();
|
||||
const { menuItems, activeMenu, setActiveMenu, setMenuItems } = useMenuStore();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// 현재 사용자 역할 가져오기
|
||||
const userDataStr = localStorage.getItem("user");
|
||||
const userData = userDataStr ? JSON.parse(userDataStr) : null;
|
||||
const currentRole = userData?.role || "CEO";
|
||||
|
||||
// 역할별 대시보드 정의
|
||||
const roleDashboards = [
|
||||
{ role: 'SystemAdmin', label: '시스템 관리자', icon: Database },
|
||||
{ role: 'CEO', label: 'CEO', icon: LayoutDashboard },
|
||||
{ role: 'ProductionManager', label: '생산 관리자', icon: Factory },
|
||||
];
|
||||
|
||||
// 초기 메뉴 설정
|
||||
useEffect(() => {
|
||||
if (menuItems.length === 0) {
|
||||
setMenuItems([
|
||||
{
|
||||
id: 'dashboard',
|
||||
label: '대시보드',
|
||||
icon: LayoutDashboard,
|
||||
path: '/dashboard'
|
||||
},
|
||||
{
|
||||
id: 'sales-leads',
|
||||
label: '영업 대시보드',
|
||||
icon: ShoppingCart,
|
||||
path: '/dashboard/sales-leads'
|
||||
},
|
||||
{
|
||||
id: 'production',
|
||||
label: '생산관리',
|
||||
icon: Factory,
|
||||
path: '/dashboard/production'
|
||||
},
|
||||
{
|
||||
id: 'quality',
|
||||
label: '품질관리',
|
||||
icon: ClipboardCheck,
|
||||
path: '/dashboard/quality'
|
||||
},
|
||||
{
|
||||
id: 'materials',
|
||||
label: '자재관리',
|
||||
icon: Package,
|
||||
path: '/dashboard/materials'
|
||||
},
|
||||
]);
|
||||
}
|
||||
}, [menuItems.length, setMenuItems]);
|
||||
|
||||
const handleMenuClick = (menuId: string, path: string) => {
|
||||
setActiveMenu(menuId);
|
||||
navigate(path);
|
||||
};
|
||||
|
||||
return (
|
||||
<SidebarProvider defaultOpen={!sidebarCollapsed}>
|
||||
<div className="flex min-h-screen w-full">
|
||||
<Sidebar>
|
||||
<SidebarHeader>
|
||||
<div className="flex items-center gap-2 px-4 py-2">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary text-primary-foreground">
|
||||
<span className="text-lg font-bold">S</span>
|
||||
</div>
|
||||
<span className="text-lg font-semibold">SAM</span>
|
||||
</div>
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
<SidebarMenu>
|
||||
{menuItems.map((item) => (
|
||||
<SidebarMenuItem key={item.id}>
|
||||
<SidebarMenuButton
|
||||
onClick={() => handleMenuClick(item.id, item.path)}
|
||||
isActive={activeMenu === item.id}
|
||||
>
|
||||
<item.icon className="h-5 w-5" />
|
||||
<span>{item.label}</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
const handleRoleChange = (role: string) => {
|
||||
// 역할 변경 시 localStorage 업데이트
|
||||
if (userData) {
|
||||
userData.role = role;
|
||||
localStorage.setItem("user", JSON.stringify(userData));
|
||||
// 페이지 새로고침하여 역할별 대시보드 표시
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
<main className="flex-1">
|
||||
<header className="sticky top-0 z-10 flex h-14 items-center gap-4 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 px-4">
|
||||
<SidebarTrigger />
|
||||
<div className="flex-1" />
|
||||
{/* 테마 토글, 사용자 메뉴 등 추가 가능 */}
|
||||
</header>
|
||||
<div className="p-6">
|
||||
<Outlet />
|
||||
</div>
|
||||
</main>
|
||||
if (menuItems.length === 0) {
|
||||
return <div className="flex items-center justify-center min-h-screen">Loading...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen w-full">
|
||||
<div className="w-64 border-r bg-gray-50 p-4">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-2xl font-bold">SAM</h1>
|
||||
</div>
|
||||
<nav>
|
||||
<ul className="space-y-2">
|
||||
{menuItems.map((item) => {
|
||||
const IconComponent = item.icon;
|
||||
return (
|
||||
<li key={item.id}>
|
||||
<button
|
||||
onClick={() => handleMenuClick(item.id, item.path)}
|
||||
className={`w-full text-left px-4 py-2 rounded-md flex items-center gap-3 transition-colors ${
|
||||
activeMenu === item.id
|
||||
? 'bg-blue-50 text-blue-900 font-medium border-l-4 border-blue-600'
|
||||
: 'hover:bg-gray-100 border-l-4 border-transparent'
|
||||
}`}
|
||||
>
|
||||
{IconComponent && <IconComponent className="h-5 w-5 flex-shrink-0" />}
|
||||
<span>{item.label}</span>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
|
||||
<main className="flex-1">
|
||||
<header className="sticky top-0 z-10 flex h-14 items-center gap-4 border-b bg-white px-4 shadow-sm">
|
||||
<button className="p-2 hover:bg-gray-100 rounded-md transition-colors">
|
||||
<Menu className="h-5 w-5" />
|
||||
</button>
|
||||
<div className="flex-1" />
|
||||
|
||||
{/* 역할별 대시보드 전환 버튼 */}
|
||||
<div className="relative group">
|
||||
<button className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 transition-colors">
|
||||
<Database className="h-4 w-4" />
|
||||
<span>{roleDashboards.find(d => d.role === currentRole)?.label || 'CEO'}</span>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
{/* 드롭다운 메뉴 */}
|
||||
<div className="absolute right-0 mt-2 w-56 bg-white border border-gray-200 rounded-md shadow-lg opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 z-50">
|
||||
{roleDashboards.map((dashboard) => {
|
||||
const DashboardIcon = dashboard.icon;
|
||||
return (
|
||||
<button
|
||||
key={dashboard.role}
|
||||
onClick={() => handleRoleChange(dashboard.role)}
|
||||
className="w-full flex items-center gap-3 px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 first:rounded-t-md last:rounded-b-md transition-colors"
|
||||
>
|
||||
<DashboardIcon className="h-4 w-4" />
|
||||
<span>{dashboard.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div className="p-6">
|
||||
<Outlet />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -38,6 +38,11 @@ export const useMenuStore = create<MenuState>()(
|
||||
}),
|
||||
{
|
||||
name: 'sam-menu',
|
||||
// menuItems는 함수(icon)를 포함하므로 localStorage에서 제외
|
||||
partialize: (state) => ({
|
||||
activeMenu: state.activeMenu,
|
||||
sidebarCollapsed: state.sidebarCollapsed,
|
||||
}),
|
||||
}
|
||||
)
|
||||
);
|
||||
@@ -26,12 +26,14 @@ export default defineConfig({
|
||||
host: '0.0.0.0', // Docker 컨테이너 내 모든 네트워크 인터페이스에서 접근 허용
|
||||
port: 5173,
|
||||
strictPort: true,
|
||||
// Nginx 리버스 프록시를 통한 도메인 접근 허용
|
||||
hmr: {
|
||||
clientPort: 80, // HTTP 사용 (nginx가 port 80에서 리스닝)
|
||||
protocol: 'ws', // HTTP 환경에서는 WebSocket (ws) 사용
|
||||
host: 'dev.sam.kr', // HMR 연결 시 사용할 호스트
|
||||
},
|
||||
// 환경별 HMR 설정: Docker 환경에서만 커스텀 설정 사용
|
||||
hmr: process.env.VITE_DOCKER_ENV === 'true'
|
||||
? {
|
||||
clientPort: 80, // HTTP 사용 (nginx가 port 80에서 리스닝)
|
||||
protocol: 'ws', // HTTP 환경에서는 WebSocket (ws) 사용
|
||||
host: 'dev.sam.kr', // HMR 연결 시 사용할 호스트
|
||||
}
|
||||
: true, // 로컬 환경에서는 기본 HMR 설정 사용
|
||||
// 파일 감시 설정 (Docker 환경에서 필수)
|
||||
watch: {
|
||||
usePolling: true,
|
||||
|
||||
Reference in New Issue
Block a user